Search code examples
angulartypescriptangular-test

Testing Angular 8 Services with `TestHttpInterceptor`?


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.

  1. Is this actually a valid testing solution (I looked at marbles but found that even less comprehensible)
  2. How can I make it work?

Solution

  • 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)