Testing Angular Services

Angular’s guide to testing is impressive both in its scope and fantastic technical communication. Much of the documentation discusses the Angular-specific testing utilities, namely the TestBed API which is crucial in testing Components and their templates.

There is, however, a callout in the testing guide that’s easy to miss:

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.

What does that mean?

In short, the guide is suggesting not to use the TestBed API for testing services.

In code, that means that instead of writing this:

beforeEach(() => {
  const bed = TestBed.configureTestingModule({
    providers: [
      UserService,
      { provide: ApiService, useClass: StubApiService }
    ]
  });

  stubApiService = bed.get(ApiService);
  service = bed.get(UserService);
});

You should be writing this:

beforeEach(() => {
  stubApiService = new StubApiService();
  service = new UserService(stubApiService as ApiService);
});

I’ll now presume you’re a free thinker and don’t plan on taking any absolute statement (from myself or the Angular team) at face value. So let’s explore a couple of reasons why: complexity and speed.

Why: Complexity

It’s clear to see that there’s a lot less going on in the pure JS test setup.

Since UserService is just an ES6 class, we can new it up as long as we have its single constructor dependency: an ApiService. Here, we’ve made a StubApiService, assigned it to a test-global variable to allow test-specific faking, and cast it as an ApiService, which is what UserService is expecting.

We get the same end result from the TestBed version, but we’re having to do a lot more to get there: We configure a TestingModule and assign it to a local variable. That includes wiring up dependency injection using the providers array, with our system under test, the UserService being provided directly, while using useClass to provide our stub in place of a real ApiService. Then we have to pull both the stub and the service instance out of the injector using the testbed.

This is objectively more code write and which means more chances to make a mistake. More important than that, though, it adds unnecessary cognitive burden unrelated to the concern of testing UserService’s functionality.

Since the outcomes of the two different mechanisms for setting up the service under test are identical, one should strongly favor the simplest solution.

Why: Speed

Certainly, with more indirection happening with the TestBed setup, there’s obviously more code running to make things happen. This of course isn’t a bad thing if the indirection is warranted and the performance impact isn’t materially different. However, with TestBed, the performance cost can be seen at even a small scale.

UserService.get() grabs a user object from ApiService, and transforms it a bit as so:

getUser(): Observable<IUser> {
  return this.apiService
    .get(userApiUri)
    .map((apiUser: IApiUser) => {
      const honorific = apiUser.isKnighted ? 'Dame' : '';
      return {
        id: apiUser.id,
        fullName: `${honorific} ${apiUser.firstName} ${apiUser.lastName}`
      };
    });
}

I tested both setups (pure JS and TestBed, as above) with identical tests:

it('should call ApiService with /user', () => {
  const getSpy = spyOn(stubApiService, 'get')
    .and.returnValue(Observable.of(stubUser));

  service.getUser();

  expect(getSpy).toHaveBeenCalledWith('/user');
});

it('should transform an ApiUser response to a User', () => {
  spyOn(stubApiService, 'get')
    .and.returnValue(Observable.of(stubUser));

  const user$ = service.getUser();

  user$.subscribe(user => {
    expect(user.fullName).toEqual('Dame Test User');
  });
});

To simulate a slightly more realistic project, I duplicated these tests 50 times each.

I found that the pure JS setup was about 10x faster than the TestBed setup, even with this very simple test case and sample size.

Pure JS: 0.05 secs
TestBed: 0.495 secs

As projects grow and add tests and complexity, test slowdown can negatively impact productivity, and worse, disincentivize adding more tests.

Conclusion

Since the outcome of setting up Angular services via TestBed or pure JS is the same, we should default to the simplest option.

PS: Did you know you can do the same for Angular Components as well? For all your tests that don’t need to assert on or manipulate the template/view or the component lifecycle, you can test the Component’s methods and explicitly pass in mocked constructor dependencies.