how do I mock out http post in Angular without usi

2020-04-01 06:02发布

问题:

How do I unit test this login function, specifically the http post part? The http mock I made is not coded correctly to get into the 'if...else' section of the code. I dont want to use TestBed. TestBed is too slow.

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

Below is the Jasmine test Im attempting: I need help with this line.

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

What should my returnValue be to get my inside of the 'if...else' code in the login function?

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

回答1:

This is the answer that I ended up using. I was using TestBed in a way that was slow when I could have been using TestBed like the code below:

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


回答2:

You can inject services into your test like

it('should do something',inject([YourService,XHRBackend],(service:YourService,mockBackend)=>{
 mockBackend.connections.subscribe((connection)=>{
   connection.mockRespond(new Response({body:JSON.stringify('your mock response')});

...

this will cause your post call to return with that mock data. What I am not certain of though is if you can actually inject XHRBackend without first doing a testbed and providing

{provide: XHRBackend, useClass:  MockBackend}


回答3:

TestBed is generally preferable way to test Angular services.

Despite what the official guide says,

Isolated unit tests examine an instance of a class all by itself without any dependence on Angular or any injected values. The tester creates a test instance of the class with new, supplying test doubles for the constructor parameters as needed, and then probes the test instance API surface.

You should write isolated unit tests for pipes and services.

isolated tests don't address DI testing. When a class is instantiated with new, its DI decorators (@Injectable, @Inject) are not tested.

Http tests are also easier to write and maintain when MockBackend is involved.

When performance becomes a real concern, some tests can be converted from TestBed to isolated. In this case Http API should be replicated with Jasmine mocks. In order to get full coverage, all functions calls should be tested. The test will look like

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

Then another test is performed with bodyMock that doesn't have access_token.

It should be noticed that localStorage should be stubbed as well in order to be properly tested. For testability reasons it's beneficial to use local storage service via DI instead.