Search code examples
angulartypescriptionic-frameworkinjectable

Unit Test of Service with HttpClient parameter raise "TypeError: _this.handler.handle is not a function"


I created a class that is an injectable service and I would like to test the functions that return an Observable object.

As soon as I try to test a function like this, I get the following error:

TypeError: _this.handler.handle is not a function

How can I manage to test this kind of function ?

I found a lot of examples on the internet, but most take the old Http module which is deprecated.

Here is my injectable class:

client.service.ts

import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders, HttpParams} from "@angular/common/http";
import {Observable} from "rxjs/Observable";


const BACKEND_PAGINATION_LIMIT = 25;

@Injectable()
/**
 * Class who make requests on Alignak backend
 * Injectable service
 */
export class BackendClient {
  token: string;
  url: string;
  http: HttpClient;

  /**
   * @param {HttpClient} http - http client for requests
   */
  constructor(http: HttpClient) {
    this.http = http;
    this.updateData()
  }

  /**
   * Update data of backend: {@link url} and {@link token}
   */
  private updateData(){
    this.token = localStorage.getItem('token');
    this.url = localStorage.getItem('url');
  }

  /**
   * GET http function
   * @param {string} endpoint - endpoint of request
   * @param {HttpParams} params - http parameters of request
   * @param {HttpHeaders} headers - htt headers of request
   * @returns {Observable<Object>} - observable object
   */
  private get(endpoint: string, params?: HttpParams, headers?: HttpHeaders): Observable<Object> {
    this.updateData();
    if (headers == null){
      headers = new HttpHeaders()
        .set('Accept', 'application/json')
        .set('Authorization', this.token);
    }
    return this.http.get(
      this.url + '/' + endpoint, {headers, params}
    )
  }

  /**
   * POST http function
   * @param {string} endpoint - endpoint of request
   * @param {Object} body - jsonable object to post
   * @returns {Observable<Object>} - observable object
   */
  private post(endpoint: string, body: Object): Observable<Object> {
    return this.http.post(this.url + '/' + endpoint, body)
  }

  /**
   * Post on "login" endpoint
   * @param {string} username - username of backend
   * @param {string} password - password of backend
   * @returns {Observable<Object>} - observable object
   */
  public login(username: string, password: string): Observable<any> {
    let body = {
      username: username,
      password: password
    };
    return this.post('login', body)
  }
}

And the corresponding test:

client.service.spec.ts

import {async, TestBed} from '@angular/core/testing';
import {HttpClient, HttpHandler} from "@angular/common/http";

import {BackendClient} from "./client.service";

describe('BackendClient Service', () => {
  let client: BackendClient;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      providers: [BackendClient, HttpClient, HttpHandler],
    });

  }));

  beforeEach(() => {
    localStorage.setItem('url', '');
    localStorage.setItem('token', '');
    client = TestBed.get(BackendClient);
  });

  // This test works
  it('Init BackendClient', () => {
    expect(client.token).toEqual('');
    expect(client.url).toEqual('');
    expect(client.http instanceof HttpClient).toBe(true);
  });

  // This test fails and expect is not take in account
  it('Login to Backend', () => {
    client.url = 'http://demo.alignak.net:5000';
    client.login('admin', 'admin')
      .subscribe(
        function (data) {
          console.log('Received data: ', data);
          expect(data['token'] != undefined).toBe(true)
        },
        err => console.log('Request ERR: ', err)
      )
  })
});

Here is the complete output error:

....LOG: 'Request ERR: ', TypeError: _this.handler.handle is not a function
TypeError: _this.handler.handle is not a function
    at MergeMapSubscriber.project (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:66308:219)
    at MergeMapSubscriber._tryNext (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:104576:27)
    at MergeMapSubscriber._next (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:104566:18)
    at MergeMapSubscriber.Subscriber.next (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:36128:18)
    at ScalarObservable._subscribe (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:128360:24)
    at ScalarObservable.Observable._trySubscribe (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:23081:25)
    at ScalarObservable.Observable.subscribe (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:23069:93)
    at MergeMapOperator.call (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:104541:23)
    at Observable.subscribe (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:23066:22)
    at FilterOperator.call (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:137708:23)
    at Observable.subscribe (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:23066:22)
    at MapOperator.call (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:130125:23)
    at Observable.subscribe (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:23066:22)
    at UserContext.<anonymous> (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:138452:14)
    at ZoneDelegate.invoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123739:26)
    at ProxyZoneSpec.onInvoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:126726:39)
    at ZoneDelegate.invoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123738:32)
    at Zone.run (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123489:43)
    at runInTestZone (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:126987:34)
    at UserContext.<anonymous> (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:127002:20)
    at ZoneQueueRunner.attempt (http://localhost:9876/base/node_modules/jasmine-core/lib/jasmine-core/jasmine.js?daba65c98fa088349a3e9d7df843a63405ccfc15:4816:44)
    at ZoneQueueRunner.QueueRunner.run (http://localhost:9876/base/node_modules/jasmine-core/lib/jasmine-core/jasmine.js?daba65c98fa088349a3e9d7df843a63405ccfc15:4854:25)
    at runNext (http://localhost:9876/base/node_modules/jasmine-core/lib/jasmine-core/jasmine.js?daba65c98fa088349a3e9d7df843a63405ccfc15:4784:18)
    at next (http://localhost:9876/base/node_modules/jasmine-core/lib/jasmine-core/jasmine.js?daba65c98fa088349a3e9d7df843a63405ccfc15:4791:11)
    at http://localhost:9876/base/node_modules/jasmine-core/lib/jasmine-core/jasmine.js?daba65c98fa088349a3e9d7df843a63405ccfc15:4709:12
    at http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:52171:17
    at ZoneDelegate.invoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123739:26)
    at AsyncTestZoneSpec.onInvoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:127212:39)
    at ProxyZoneSpec.onInvoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:126723:39)
    at ZoneDelegate.invoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123738:32)
    at Zone.run (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123489:43)
    at AsyncTestZoneSpec.finishCallback (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:52166:25)
    at http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:127155:31
    at ZoneDelegate.invokeTask (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123772:31)
    at Zone.runTask (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123539:47)
    at ZoneTask.invokeTask (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123847:34)
    at ZoneTask.invoke (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:123836:48)
    at timer (http://localhost:9876/base/test-config/karma-test-shim.js?c3c7bb07d085cf3d6c123f55b228352de66aee6a:125405:29)

It looks like the handler of the HttpClient client is not defined ... and the test in the subscribe is not taken into account.

Solution with Mock:

import {TestBed, getTestBed} from '@angular/core/testing';
import {HttpClient} from "@angular/common/http";
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

import {BackendClient} from "./client.service";

describe('BackendClient Service', () => {

  let injector: TestBed;
  let service: BackendClient;
  let httpMock: HttpTestingController;

  beforeEach(() => {

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [BackendClient]
    });
    injector = getTestBed();
    localStorage.setItem('token', 'my-long-token');
    localStorage.setItem('url', 'http://demo.alignak.net:5000');
    service = injector.get(BackendClient);
    httpMock = injector.get(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('Init BackendClient', () => {
    expect(service.token).toEqual('my-long-token');
    expect(service.url).toEqual('http://demo.alignak.net:5000');
    expect(service.http instanceof HttpClient).toBe(true);
  });

  // This test fails and expect is not take in account
  it('Login to Backend', () => {
    const dummyToken = [
      { token: 'my-received-token' }
    ];

    service.login('admin', 'admin').subscribe(
      token => {
      expect(token.length).toBe(1);
      expect(token).toEqual(dummyToken);
    });

    const req = httpMock.expectOne(`${service.url}/login`);
    expect(req.request.method).toBe("POST");
    expect(req.request.url).toBe(`${service.url}/login`);
    expect(req.request.body).toEqual({username: 'admin', password: 'admin'});
    req.flush(dummyToken);
  })
});

Solution

  • First of all :

    http: HttpClient;
    constructor(http: HttpClient) {
      this.http = http;
    }
    

    This is duplicate code. Putting a variable as a parameter to the constructor creates a member of the class. Here, you're creating it twice. I'm flabbergasted that your linter doesn't tell you that you have a shadowed variable.

    Second, when you want to test a service that make HTTP calls, you're supposed to mock your backend. Here, you're not mocking it, you're making real HTTP calls. That's not unit testing.

    If you don't know how to mock your backend, a quick google search will give you results like this one.

    Finally, this error comes from the fact that you have an interceptor that intercepts your requests. Mocking your backend will get rid of the said interceptor, removing your issue.