Write angular2 tests and changing the mock return

2019-04-24 12:21发布

问题:

I'm writing some tests for a service and I'm altering the response from mock functions to test various cases. At the moment, every time I want to change the response of a mock, I need to reset the TestBed and configure the testing module again, injecting my new Mocks as dependencies.

I feel like there must be a DRYer way to write this spec, but I can't figure it out. Does anyone have any ideas?

(I understand that I could write tests for this service as a standard ES6 class, but I get the same scenario with my components and services that use the Http response mocking stuff from angular.)

Here is my spec file:

import { TestBed, inject } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { UserService, RestService } from '../index';
import { User } from '../../../models/index';


let getUserSpy = jasmine.createSpy('getUser');
let upsertUserSpy = jasmine.createSpy('upsertUser');


// NOTE that initally, the MockRestService throws errors for all responses
class MockRestService {
  getUser = getUserSpy.and.returnValue(Observable.throw('no thanks'));
  upsertUser = upsertUserSpy.and.returnValue(Observable.throw('no thanks'));
}


describe('User service - ', () => {

  let service;

  /**
   * First TestBed configuration
   */
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        UserService,
        {
          provide: RestService,
          useClass: MockRestService,
        }
      ]
    });
  });

  beforeEach(inject([UserService], (user: UserService) => {
    service = user;
  }));

  /* ... tests ... */

  describe('getUser/ upsertUser succeeds with INVALID user - ', () => {

    /**
     * Altering mock
     */
    class MockRestService {
      getUser = getUserSpy.and.returnValue(Observable.of({json: () => {
        return {name: 'dave'};
      }}));
      upsertUser = upsertUserSpy.and.returnValue(Observable.of({json: () => {}}));
    }

    /**
     * Reset and reconfigure TestBed. Lots of repetition!
     */
    beforeEach(() => {
      TestBed.resetTestingModule();
    });

    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          UserService,
          {
            provide: RestService,
            useClass: MockRestService,
          }
        ]
      });
    });

    beforeEach(inject([UserService], (user: UserService) => {
      service = user;
    }));

    /* ... tests ... */

  });

  describe('getUser/upsertUser succeeds with valid user', () => {
    const validResponse = {
      json: () => {
        return {
          firstName: 'dave',
          lastName: 'jones',
          email: 'dave@gmail.com'
        };
      }
    };

    /**
     * Altering mock
     */
    class MockRestService {
      getUser = getUserSpy.and.returnValue(Observable.of(validResponse));
      upsertUser = upsertUserSpy.and.returnValue(Observable.of(validResponse));
    }

    /**
     * Reset and reconfigure testbed. Lots of repetition!
     */
    beforeEach(() => {
      TestBed.resetTestingModule();
    });

    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          UserService,
          {
            provide: RestService,
            useClass: MockRestService,
          }
        ]
      });
    });

    beforeEach(inject([UserService], (user: UserService) => {
      service = user;
    }));

    /* ... tests ... */

  });

});

回答1:

It could be some variation of

function setupUserTestbed() {
    beforeEach(() => {
      TestBed.configureTestingModule({...});
    });

    afterEach(() => {
      TestBed.resetTestingModule();
    });
 }

...
setupUserTestbed();
...
setupUserTestbed();

But the purpose of describe blocks (besides grouping the specs in test report) is to arrange before* and after* blocks in a way they are most efficient.

If top-level describe block has beforeEach block, you can be sure that it affects the specs in nested describe blocks. If describe blocks are siblings, common behaviour should be moved to top-level describe. If there's no top-level describe for sibling describe blocks, it should be created.

In posted code top-level describe('User service - ', () => { ... }) already has beforeEach blocks with TestBed.configureTestingModule, TestBed.resetTestingModule (it should be performed in afterEach) and inject. There's no need to duplicate them in nested describe blocks.

The recipe for MockRestService class is the same as for any mock that alternates between specs. It should be a let/var variable:

describe(...
  let MockRestService = class MockRestService { ... };

  beforeEach(() => { Testbed... });

  describe(...
    MockRestService = class MockRestService { ... };

    beforeEach(inject(...));

There can be a lot of variations of this pattern. The class itself may be constant, but getUser and upsertUser properties may alternate:

let getUserSpy;
let upsertUserSpy;

class MockRestService {
  getUser = getUserSpy;
  ...
}

describe(...

  beforeEach(() => { Testbed... });

  beforeEach(() => {
    getUserSpy = jasmine.createSpy().and.returnValue(...);
    ...
  });

  describe(...
    beforeEach(() => {
      getUserSpy = jasmine.createSpy().and.returnValue(...);
      ...
    });

    beforeEach(inject(...));

This also solves an important issue, because spies should be fresh in each spec, i.e. be defined in beforeEach. getUserSpy and upsertUserSpy can be re-assigned after Testbed configuration but before inject (this is where MockRestService class is likely instantiated).