10

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 ... */

  });

});
Kim Kern
  • 54,283
  • 17
  • 197
  • 195
dafyddPrys
  • 898
  • 12
  • 23

1 Answers1

5

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

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Thanks, I have moved my `resetTestingModule` function into an `afterEach` block in the top level describe. However, when I remove the duplicate `beforeEach` blocks from my nested `describe`s, my modified mock class `MockRestService`, which is defined in the nested `describe`, does not get injected. Have I missed your point? – dafyddPrys Jan 13 '17 at 11:46
  • I think the problem is that you can only configure the test bed once without resetting it. So If I have `configureTestBed` in the outer `describe`, I cannot call it again in a nested `describe` without resetting it first. – dafyddPrys Jan 13 '17 at 11:53
  • If the code in `beforeEach` blocks is totally duplicated, they shouldn't be repeated (this results in double bootstrapping). The pattern for `MockRestService` class is the same as for any mock variable that should be changed between specs. Either the class or its internals (getUserSpy, ...) should be re-assigned in nested `describe`s. I've updated the answer to explain this. – Estus Flask Jan 13 '17 at 14:54
  • Ah I understand. I wasn't confident defining a class with a let, but now I know I can. Thank you! – dafyddPrys Jan 13 '17 at 15:51