I have a fairly small Angular front-end, which gets a lot of it's data from an external server.
Tests are giving my gyp
I can test a simple component by mocking the service, however this is a blanket "replace the output" solution, and not a real test... for that, I believe I need to provide a known return when the service calls the external API.
Here's a simple example:
The object/interface definition:
// alerts.ts
export interface Alert {
id: number;
display_message: string;
is_enabled: boolean;
}
The service definition:
// alerts.service.ts
import { of as observableOf, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Alert } from './alerts';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
export interface IAlertService {
getAlerts(): Observable<Alert[] | null>;
}
@Injectable()
export class AlertService implements IAlertService {
readonly baseUrl = '/api/alerts/';
alerts$: Observable<Alert[] | null>;
constructor(private _http: HttpClient) { }
getAlerts(): Observable<Alert[] | null> {
return this._http.get<Alert[] | null>(this.baseUrl).pipe(
catchError(error => {
console.log('in catch: ', error);
return observableOf(null);
}));
}
}
The component code:
// alerts/alerts.component.html
<div *ngIf="alerts" >
<div class="service-notice" *ngFor="let alert of alerts">
<p [innerHTML]="alert.display_message"></p>
</div>
</div>
and
// alerts/alerts.component.ts
import { Component, OnInit } from '@angular/core';
import { Alert } from '../alerts';
import { AlertService } from '../alerts.service';
@Component({
selector: 'naas-alerts',
templateUrl: './alerts.component.html',
styleUrls: ['./alerts.component.scss'],
})
export class AlertComponent implements OnInit {
alerts: Alert[] | null;
constructor(private alertService: AlertService) { }
ngOnInit() {
this.alertService.getAlerts().subscribe(data => {
this.alerts = data;
});
}
}
I then wrote an httpInterceptor class so I could define the string I wanted returned within the test:
// testing_interceptors.ts
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HTTP_INTERCEPTORS
} from '@angular/common/http';
import { Injectable, Injector } from '@angular/core';
import { of as observableOf, Observable } from 'rxjs';
@Injectable()
export class TestHttpInterceptor implements HttpInterceptor {
current_containers: string = '[]';
current_user: string = '';
alerts: string = '[]';
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('TestHttpInterceptor called');
const url_regex = /(\/api(?:\/[\w+\/]+)+)$/;
const url_path = url_regex.exec(request.url)[1];
if(request.method === "GET") {
if(url_path == '/api/alerts/') {
return observableOf(new HttpResponse({
status: 200,
body: this.alerts
}));
}
if(url_path == '/api/users/current/') {
return observableOf(new HttpResponse({
status: 200,
body: this.current_user
}));
}
if (url_path === '/api/users/current/containers/') {
return observableOf(new HttpResponse({
status: 200,
body: this.current_containers
}));
}
}
}
}
... and my test (yes, I've still got some of the old _component_mock_ commented out):
// alerts/alerts.component.spec.test
import { HttpClientModule, HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { AlertComponent } from './alerts.component';
import { AlertService } from '../alerts.service';
import { AlertServiceMock } from '../alerts.service.mock';
import { TestHttpInterceptor } from '../testing_interceptors';
describe('AlertsComponent', () => {
let fixture: ComponentFixture<AlertComponent>;
let component: AlertComponent;
// let alertServiceMock: AlertServiceMock;
let testHttpInterceptor: TestHttpInterceptor;
beforeEach(() => {
// alertServiceMock = new AlertServiceMock();
testHttpInterceptor = new TestHttpInterceptor();
TestBed.configureTestingModule({
declarations: [ AlertComponent ],
providers: [
{provide: AlertService, useClass: AlertService },
{provide: HttpClient, useClass: HttpClientModule},
{provide: HTTP_INTERCEPTORS, useClass: TestHttpInterceptor, multi: true }
],
}).compileComponents();
fixture = TestBed.createComponent(AlertComponent);
component = fixture.componentInstance;
});
it('should be created', done => {
fixture.detectChanges();
expect(component).toBeTruthy();
done();
});
// it('should have no alerts with no data', () => {
// alertServiceMock.test_alert = null;
// fixture.detectChanges();
// const compiled = fixture.debugElement.queryAll(By.css('p'));
// expect(compiled.length).toBe(0);
// });
// it('should have one alert', () => {
// alertServiceMock.test_alert = [{
// id: 1,
// display_message: 'Foo',
// is_enabled: true,
// }];
// fixture.detectChanges();
// const compiled = fixture.debugElement.queryAll(By.css('p'));
// expect(compiled.length).toBe(1);
// });
});
The problem is, when I run this, I get the following error:
TypeError: this.http.get is not a function
error properties: Object({ ngDebugContext: DebugContext_({ view: Object({ def: Object({ factory: Function, nodeFlags: 33669121, rootNodeFlags: 33554433, nodeMatchedQueries: 0, flags: 0, nodes: [ Object({ nodeIndex: 0, parent: null, renderParent: null, bindingIndex: 0, outputIndex: 0, checkIndex: 0, flags: 33554433, childFlags: 114688, directChildFlags: 114688, childMatchedQueries: 0, matchedQueries: Object({ }), matchedQueryIds: 0, references: Object({ }), ngContentIndex: null, childCount: 1, bindings: [ ], bindingFlags: 0, outputs: [ ], element: Object({ ns: '', name: 'naas-alerts', attrs: [ ], template: null, componentProvider: Object({ nodeIndex: 1, parent: <circular reference: Object>, renderParent: <circular reference: Object>, bindingIndex: 0, outputIndex: 0, checkIndex: 1, flags: 114688, childFlags: 0, directChildFlags: 0, childMatchedQueries: 0, matchedQueries: Object, matchedQueryIds: 0, references: Object, ngContentIndex: -1, childCount: 0, bindings: Array, bindingFlags: 0, outputs: Array ...
at <Jasmine>
at AlertService.getAlerts (http://localhost:9876/_karma_webpack_/src/app/alerts.service.ts:20:22)
at AlertComponent.ngOnInit (http://localhost:9876/_karma_webpack_/src/app/alerts/alerts.component.ts:18:23)
at checkAndUpdateDirectiveInline (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:31910:1)
at checkAndUpdateNodeInline (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:44367:1)
at checkAndUpdateNode (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:44306:1)
at debugCheckAndUpdateNode (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:45328:36)
at debugCheckDirectivesFn (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:45271:1)
at Object.eval [as updateDirectives] (ng:///DynamicTestModule/AlertComponent_Host.ngfactory.js:10:5)
at Object.debugUpdateDirectives [as updateDirectives] (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:45259:1)
at checkAndUpdateView (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:44271:1)
Chrome 80.0.3987 (Linux 0.0.0): Executed 16 of 23 (1 FAILED) (0 secs / 2.133 secs)
Chrome 80.0.3987 (Linux 0.0.0) AlertsComponent should be created FAILED
TypeError: this.http.get is not a function
error properties: Object({ ngDebugContext: DebugContext_({ view: Object({ def: Object({ factory: Function, nodeFlags: 33669121, rootNodeFlags: 33554433, nodeMatchedQueries: 0, flags: 0, nodes: [ Object({ nodeIndex: 0, parent: null, renderParent: null, bindingIndex: 0, outputIndex: 0, checkIndex: 0, flags: 33554433, childFlags: 114688, directChildFlags: 114688, childMatchedQueries: 0, matchedQueries: Object({ }), matchedQueryIds: 0, references: Object({ }), ngContentIndex: null, childCount: 1, bindings: [ ], bindingFlags: 0, outputs: [ ], element: Object({ ns: '', name: 'naas-alerts', attrs: [ ], template: null, componentProvider: Object({ nodeIndex: 1, parent: <circular reference: Object>, renderParent: <circular reference: Object>, bindingIndex: 0, outputIndex: 0, checkIndex: 1, flags: 114688, childFlags: 0, directChildFlags: 0, childMatchedQueries: 0, matchedQueries: Object, matchedQueryIds: 0, references: Object, ngContentIndex: -1, childCount: 0, bindings: Array, bindingFlags: 0, outputs: Array ...
at <Jasmine>
at AlertService.getAlerts (http://localhost:9876/_karma_webpack_/src/app/alerts.service.ts:20:22)
at AlertComponent.ngOnInit (http://localhost:9876/_karma_webpack_/src/app/alerts/alerts.component.ts:18:23)
at checkAndUpdateDirectiveInline (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:31910:1)
at checkAndUpdateNodeInline (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:44367:1)
at checkAndUpdateNode (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:44306:1)
at debugCheckAndUpdateNode (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:45328:36)
at debugCheckDirectivesFn (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:45271:1)
at Object.eval [as updateDirectives] (ng:///DynamicTestModule/AlertComponent_Host.ngfactory.js:10:5)
at Object.debugUpdateDirectives [as updateDirectives] (http://localhost:9876/_karma_webpack_/node_modules/@angular/core/fesm2015/core.js:45259:1)
Chrome 80.0.3987 (Linux 0.0.0): Executed 23 of 23 (1 FAILED) (2.504 secs / 2.346 secs)
... I've been round & round T'Internet, and found lots on writing interceptors... but less on testing them.
I've spend far too long on this, and could do with advice.
marbles
but found that even less comprehensible)This is what worked for me...
I specifically wanted to test the html the component produced, for a given response from the external API call - ie, I want to test the code in alerts.component.ts
as much as possible
// alerts.component.spce.ts
import { By } from '@angular/platform-browser';
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Type } from '@angular/core';
import { AlertComponent } from './alerts.component';
import { AlertService } from '../alerts.service';
/*
This tests alerts.component.ts.
AlertComponent has an ngOnInit method, which uses AlertService.getAlerts
AlertService.getAlerts calls `/api/alerts/`
.... HOWEVER the HttpTestingController catches it & flushes back our canned
reponse.
This response is processed by AlertService.getAlerts & returned to
AlertComponent - which builds the fragment of html defined in
alerts.components.html - which we can test.
*/
describe('AlertsComponent', () => {
let fixture: ComponentFixture<AlertComponent>;
let httpMock: HttpTestingController;
let alertComponent: AlertComponent;
beforeEach( async () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [ AlertComponent ],
providers: [ AlertService ],
});
await TestBed.compileComponents();
fixture = TestBed.createComponent(AlertComponent);
alertComponent = fixture.componentInstance;
httpMock = fixture.debugElement.injector
.get<HttpTestingController>(HttpTestingController as Type<HttpTestingController>);
fixture.detectChanges();
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
alertComponent.ngOnInit();
const reqs = httpMock.match(`/api/alerts/`);
for (const req of reqs) {
req.flush(null);
}
fixture.detectChanges();
expect(alertComponent).toBeTruthy();
});
it('The component should init, call the alert service, and get a response', () => {
const dummyAlerts = [{
id: 1,
display_message: 'Foo',
is_enabled: true,
}];
alertComponent.ngOnInit();
const reqs = httpMock.match(`/api/alerts/`);
for (const req of reqs) {
req.flush(dummyAlerts);
}
fixture.detectChanges();
const compiled = fixture.debugElement.queryAll(By.css('p'));
expect(compiled.length).toBe(1);
});
it('The component should build 2 alerts from a response', () => {
const dummyAlerts = [{
id: 1,
display_message: 'Foo',
is_enabled: true,
}, {
id: 2,
display_message: 'Bar',
is_enabled: true,
}];
alertComponent.ngOnInit();
const reqs = httpMock.match(`/api/alerts/`);
for (const req of reqs) {
req.flush(dummyAlerts);
}
fixture.detectChanges();
const compiled = fixture.debugElement.queryAll(By.css('p'));
expect(compiled.length).toBe(2);
});
// ## This fails when looking for 'small' - needs investigated ##
it('The component should build 2 alerts from a response', () => {
const dummyAlerts = [{
id: 1,
display_message: '<small>Foo</small>',
is_enabled: true,
}];
alertComponent.ngOnInit();
const reqs = httpMock.match(`/api/alerts/`);
for (const req of reqs) {
req.flush(dummyAlerts);
}
fixture.detectChanges();
const compiled = fixture.debugElement.queryAll(By.css('p'));
expect(compiled.length).toBe(1);
});
});
(I'd be delighted to be told better ways of doing this)