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

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

我创建了一个 class 可注入服务,我想测试 return Observable 对象的功能。

当我尝试测试这样的函数时,我收到以下错误:

TypeError: _this.handler.handle is not a function

我怎样才能测试这种功能?

我在互联网上找到了很多示例,但大多数都采用了已弃用的旧 Http 模块。

这是我的注射剂 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)
  }
}

以及相应的测试:

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)
      )
  })
});

这里是完整的输出错误:

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

似乎没有定义 HttpClient 客户端的处理程序...并且没有考虑订阅中的测试。

模拟解决方案:

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);
  })
});

首先:

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

这是重复的代码。将变量作为构造函数的参数创建 class 的成员。在这里,您要创建它两次。我很惊讶你的 linter 没有告诉你你有一个阴影变量。

其次,当您想要测试进行 HTTP 调用的服务时,您应该模拟您的后端。在这里,您不是在嘲笑它,而是在进行真正的 HTTP 调用。那不是单元测试。

如果您不知道如何模拟后端,快速 google 搜索会给您结果 like this one

最后,这个错误是因为你有一个拦截器拦截了你的请求。模拟您的后端将摆脱上述拦截器,从而消除您的问题。