【问题标题】:how do I mock out http post in Angular without using TestBed?如何在不使用 TestBed 的情况下在 Angular 中模拟 http 帖子?
【发布时间】:2017-05-23 02:09:52
【问题描述】:

如何对这个登录功能进行单元测试,特别是 http post 部分?我制作的 http 模拟未正确编码以进入代码的“if...else”部分。我不想使用 TestBed。 TestBed 太慢了。

login(username: string, password: string): Observable<boolean> {

    const headers = new Headers();
    headers.append('Content-Type', 'application/json');
    headers.append('accept', 'application/json');
    let options = new RequestOptions({ headers: headers });

    return this.http.post('https://blah/api/login',
      JSON.stringify({ username: username, password: password }), options)
        .map((response: Response) => {
            const token = response.json() && response.json().access_token;
            if (token) {
                this.token = token;
                localStorage.setItem('currentUser', JSON.stringify({ username: username, token: token }));
                return true;
            } else {
                return false;
            }
        }).catch(this._serverError);
}

private _serverError(err: any) {
    return Observable.throw(err || 'backend server error');
}

下面是我正在尝试的 Jasmine 测试:我需要有关此行的帮助。

spyOn(mockHttp,'post').and.returnValue(Observable.of(response));

我的 returnValue 应该是什么才能让我进入登录函数中的“if...else”代码?

    describe('AuthenticationService', () => {
      let service: AuthenticationService;
      let mockHttp = null;

      beforeEach(() => {
        mockHttp = {};
        mockHttp.post = function(){};
        service = new AuthenticationService(mockHttp);
     });

    it(`should set access token in local storage for successful login`,() => {
      const access_token = 'blah83balc380';
      const responseOptions =  new ResponseOptions();
      responseOptions.status = 200;
      responseOptions.body = {access_token:access_token};
      const username = 'test';
      const currentUserExpected = JSON.stringify({ username: username, token: access_token });
      var response = new Response(responseOptions);
      spyOn(mockHttp,'post').and.returnValue(Observable.of(response));
      service.login(username, 'test');
      var currentUser = localStorage.getItem('currentUser');
      expect(currentUserExpected).toEqual(currentUser);
    });
  });

【问题讨论】:

  • 是什么让你想做这样的事情?它需要您模拟json() 函数等。这很麻烦。由于您无法处理它,这可以被认为是使用 TestBed 进行测试的证明。顺便说一句,代码格式错误,问题不可读。
  • @estus TestBed 测试很慢。使用 TestBed 运行 100 个测试需要多长时间?太长。这就是为什么我不想使用 TestBed 除非绝对必要。使用 TestBed 进行测试并不优越。您的评论无用且无益。
  • 如果您不欣赏它,这不会使评论毫无用处。如果不提供全面覆盖,100 次测试的效率如何?您可以使用 TestBed 正确测试 DI。没有它你不能这样做。甚至没有提及您在问题中遇到性能问题...等等,有提及,但由于格式错误,它不可见。
  • @Maxime 谢谢!那篇文章是一个很好的总结,正是我所需要的

标签: angular unit-testing typescript jasmine rxjs


【解决方案1】:

这是我最终使用的答案。当我可以像下面的代码一样使用 TestBed 时,我使用 TestBed 的方式很慢:

import { TestBed, inject} from '@angular/core/testing';
import { HttpModule,  XHRBackend, Response, ResponseOptions } from '@angular/http';
import { AuthenticationService } from '../_services/authentication.service';
import { MockBackend } from '@angular/http/testing';

describe('AuthenticationService', () => {
  let mockbackend, service;

  beforeEach(() => {
    localStorage.clear();
    TestBed.configureTestingModule({
      imports: [ HttpModule ],
      providers: [
        AuthenticationService,
        { provide: XHRBackend, useClass: MockBackend }
      ]
    });
  });

  beforeEach(inject([AuthenticationService, XHRBackend], (_service, 
_mockbackend) => {
    service = _service;
    mockbackend = _mockbackend;
  }));

  it('should set access token in local storage for successful login', () => 
 {
    const access_token = 'blah83balc380';
    const username = 'test';
    const currentUserExpected = JSON.stringify({ username: username, token: access_token });
    const response = {access_token: access_token};
    const responseOptions = new ResponseOptions();
    responseOptions.body = JSON.stringify(response);
    mockbackend.connections.subscribe(connection => {
      connection.mockRespond(new Response(responseOptions));
    });
    service.login(username, 'test').subscribe(respond => {
      expect(respond).toEqual(true);
      const currentUser = localStorage.getItem('currentUser');
      expect(currentUserExpected).toEqual(currentUser);
    });
  });

it('should not set access token in local storage for unsuccessful login', () =>     {
    const username = 'test';
    const responseOptions = new ResponseOptions();
    responseOptions.body = '';
    responseOptions.status = 401;
    const response = new Response(responseOptions);
    response.ok = false;
    response.statusText = 'Unauthorized';
    response.type = 2;

    mockbackend.connections.subscribe(connection => {
      connection.mockRespond(response);
    });
    service.login(username, 'test').subscribe(respond => {
      expect(respond).toEqual(false);
    }, err => {
      const currentUser = localStorage.getItem('currentUser');
      expect(currentUser).toEqual(null);
    });
  });
});

【讨论】:

  • 我只在每个测试中使用 TestBed.createComponent,我不需要这样做。 TestBed.createComponent 很慢,但答案中的方法很快......好吧,足够快。
  • 您好,我已尝试实施您的解决方案,但遇到了一些错误。你能看看这个帖子吗:stackoverflow.com/questions/53494266/…
【解决方案2】:

您可以像

一样将服务注入到您的测试中
it('should do something',inject([YourService,XHRBackend],(service:YourService,mockBackend)=>{
 mockBackend.connections.subscribe((connection)=>{
   connection.mockRespond(new Response({body:JSON.stringify('your mock response')});

...

这将导致您的 post 调用返回该模拟数据。我不确定的是,您是否可以在不首先进行测试平台和提供的情况下实际注入 XHRBackend

{provide: XHRBackend, useClass:  MockBackend}

【讨论】:

  • 我几天前尝试过这种方法。如果不使用 TestBed,我无法让“提供”工作。不过感谢您的想法!
【解决方案3】:

TestBed 通常是测试 Angular 服务的首选方式。

不管官方指南怎么说,

独立的单元测试完全独立地检查一个类的实例,而不依赖于 Angular 或任何注入的值。测试器使用 new 创建类的测试实例,根据需要为构造函数参数提供测试替身,然后探测测试实例 API 表面。

您应该为管道和服务编写独立的单元测试。

独立测试不涉及 DI 测试。当使用new 实例化一个类时,它的 DI 装饰器(@Injectable@Inject)不会被测试。

Http 测试在涉及MockBackend 时也更容易编写和维护。

当性能成为一个真正的问题时,一些测试可以从TestBed 转换为隔离的。在这种情况下,Http API 应该使用 Jasmine 模拟来复制。为了获得全面覆盖,应测试所有函数调用。测试看起来像

  mockHttp = jasmine.createSpyObj(['post']);
  service = new AuthenticationService(mockHttp);
  ...

  it(..., fakeAsync(async () => {
    const bodyMock = { access_token: 'foo' };
    const responseMock = { json: jasmine.createSpy().and.returnValue(bodyMock) };
    const responseMock$ = Observable.of(responseMock);
    mockHttp.post.and.returnValue(responseMock$);
    
    const login$ = service.login(...);
    
    expect(mockHttp.post).toHaveBeenCalledTimes(1);
    
    const postArgs = callback.calls.first().args;
    expect(postArgs).toEqual([..., ..., jasmine.any(RequestOptions));
    
    const requestOptions = postArgs[2];
    expect(requestOptions.headers).toEqual(jasmine.any(Headers));
    expect(Array.from(requestOptions.headers._headers)).toEqual([
      ['Content-Type', ['application/json']],
      ['accept', ['application/json']]
    ]);
    
    expect(login$).toEqual(jasmine.any(Observable));
    const login = await login$.toPromise();

    expect(responseMock.json).toHaveBeenCalled();
    expect(service.token).toBe('foo');
    expect(localStorage.setItem).toHaveBeenCalledWith(...);
    expect(login).toBe(true);
  }));

然后使用没有access_tokenbodyMock 执行另一个测试。

应该注意localStorage 也应该被存根以便正确测试。出于可测试性的原因,通过 DI 使用本地存储服务是有益的。

【讨论】:

    猜你喜欢
    • 2018-02-01
    • 1970-01-01
    • 2023-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-25
    • 2016-12-19
    • 1970-01-01
    相关资源
    最近更新 更多