Write angular2 tests and changing the mock return values - make it DRY?
Asked Answered
V

1

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: '[email protected]'
        };
      }
    };

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

  });

});
Venavenable answered 10/1, 2017 at 11:33 Comment(0)
I
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).

Inward answered 10/1, 2017 at 17:58 Comment(4)
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 describes, my modified mock class MockRestService, which is defined in the nested describe, does not get injected. Have I missed your point?Venavenable
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.Venavenable
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 describes. I've updated the answer to explain this.Inward
Ah I understand. I wasn't confident defining a class with a let, but now I know I can. Thank you!Venavenable

© 2022 - 2024 — McMap. All rights reserved.