Unit Testing in Angular: Stubs vs Spies vs Mocks

Unit testing is a very important topic in the world of software development. It gives us feedback about the design of our code (difficulty to test a code means bad design). But most importantly, it gives us confidence that we can refactor our application, or add new features to it, without breaking existing functionalities.

It is very rare that the units we are testing do not have dependencies (or collaborators as called in the literature). Therefore, most of the time, we are required to provide test doubles for immediate dependencies of the System Under Test (SUT), in other words, the class we are testing. There are many ways to provide those test doubles, depending on the testing framework we are using. Also many different terms are involved; there are many schools of thought about how things should be done.

In Angular, the default testing framework is Jasmine. It comes out of the box when generating an Angular project using the Angular CLI.
Jasmine is a behavior-driven development framework for testing JavaScript code.

In this article, I will show you the differences between the various kinds of test doubles, and how to use them when unit testing Angular applications using Jasmine.

The example used in this Article

  1. We use the ngFor directive to loop over an array of Team.
  2. To fetch the list of teams, we inject the TeamService.
  3. We use the ngOnInit lifecycle hook to invoke the service's getTeams method. This method returns an Observable of Team[].

TeamListComponent is what we are going to test; it is our SUT. It depends on TeamService. Therefore TeamService is the collaborator.

Before diving into the actual implementation of the tests, let's go over some important theory.

Solitary vs sociable tests

In an article called Unit Test, Martin Fowler puts the emphasize on the distinction between the two types of unit testing: solitary and sociable unit testing.

Sociable unit tests

Sociable unit tests are unit tests that use real instances of their dependencies. When using this kind of testing, we assume the correctness of your SUT's dependencies.

  1. This service returns hard-coded data. So it doesn't need to be doubled.

When testing a system that depends on this implementation of TeamService, we don't need to use a test double. This example is not realistic, because we hard-coded the data. But it illustrates well the case where we can use a real instance of the collaborator.

It is not always possible, nor is it a good idea, to make all your unit tests sociable. If the collaboration between the SUT and its dependencies is awkward, then test doubles should be used instead.

Solitary unit tests

Solitary unit tests are unit tests that do not use real instances of their dependencies. For example, when you are testing an Angular service that depends on the HttpClient service, you don't want to hit the real back end in your unit tests.

  1. Inject the Angular's HttpClient
  2. Make a GET request to the back end.

When testing a class that injects this service, using the real HttpClient instance would make your tests brittle and non-deterministic, as their result would depend on the responses of your back end.

Sociable or solitary in Angular?

In Angular, I note that many people find it difficult to use real instances of dependencies. This is mainly due to a lack of separation of concerns. It is not rare to see an Angular service, let's say AuthService, that inject Router, HttpClient, TranslateService, ToasterService, and the list goes on.
Instead we should thrive to create services that do one thing and do it well. Paradigms like Redux, and libraries like NgRx, could surely help. But that is a topic for another day.

My rule of thumb is to use the real instances if I don't need to import many modules, or provide many services, when setting up the tests. And if the collaborator is stable, i.e. it is not accessing file systems or making network calls.

The differences between stubs, spies and mocks

Once again I will refer to the definitions from another Martin Fowler's article called Test Double. Actually, those terms come from Gerard Meszaros.

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

So a stub is a function that replaces a real implementation of an existing function. Stubbing is, generally, an operation local to a test. We use stubs if we want to:

  • control individual method behavior for a specific test case,
  • prevent a method from making side effects like communicating with the outside world using Angular's HttpClient.

  1. We create an instance of our collaborator by invoking its constructor. We pass it the undefined primitive to fill the parameters list because otherwise the TypeScript compiler would yell at us. We don't care about the actual value passed to the service's constructor. undefined is called a dummy in the literature.
  2. We stub the getTeams method with a function that returns an Observable of Team.
  3. We then act on the SUT by invoking the ngOnInit() lifecycle hook.
  4. We finish off by asserting that the teams$ property contains the correct Observable.

Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.

First of all, a spy is a stub. So what we said previously about stubs also applies to spies. The added benefit of spies is that they allow to watch how the function is used. We use spies when we want to track:

  • if a function has been called by the SUT,
  • how many times it has been called,
  • which arguments were passed.

  1. Spy on the collaborator's getTeams method. The default behavior of Jasmine spies is not to call the original function. This is tantamount to transforming the getTeams into a method with an empty body.
  2. We act on the SUT by invoking the ngOnInit() lifecycle hook.
  3. We check that the method getTeams of the collaborator have been called by the SUT.

Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.

Unlike stubs and spies a mock verifies itself that it has been used correctly by the SUT. Therefore mocks are often tightly coupled to implementation details, thus making your code harder to refactor. We will want to use mock if we want to test the interaction of our SUT with a collaborator that communicate with the outside world.

  1. We create a mock object by calling sinon.mock and passing it the collaborator we want to mock. For this example, I used Sinon.JS, because we can't create mocks in Jasmine. Such a concept just doesn't exist.
  2. Next, we setup expectations on the mock. We are expecting that the method getTeams will be called exactly once in the lifespan of this test.
  3. Then, we exercise the SUT by invoking the ngOnInit() lifecycle hook.
  4. Finally, the mock itself verifies that expectations we set on it are met. If not mock.verify() will throw an exception and fails our test.

Most of time, you will want to use mocks when testing HTTP request. That's why Angular provides out-of-the-box a way to mock the HttpClient with the use of the HttpTestingController.

Summary of the differences between stubs, spies and mocks

To summarize this section, I will say that mocks and spies insist on behavior (which methods were called and how?) while stubs put the emphasis on state (what is the result of calling those methods?). The difference between spies and mocks is that with mocks the verification is automatic and done by the mock itself while you have to manually verify your spies.

With that said, should we use stubs, spies or mocks ? This question has led to the dichotomy between Classicism and Mockism. Classicists use real instances when they can while mockists always use mocks. Classicism goes along with sociable testing and Mockism with solitary testing. If you want to learn more about the subject, check out Martin Fowler's article called Mocks Aren't Stubs where he delves on the opposition between the two schools of thought.

In a nutshell

In a nutshell, Jasmine is a spy-based testing framework because only the notion of spy exists in Jasmine. There are no stubs in Jasmine, because there is no need to. The dynamic nature of JavaScript allows us to change the implementation of a JavaScript function on the fly. We can also stub functions using spies. What we can't really do, in Jasmine, is to mock objects. The concept of mock doesn't exist in Jasmine.

If you are a mockist, maybe you will want to use another testing framework. Another option is to use, in conjunction with Jasmine, Sinon.JS (or other alternatives). Sinon.JS provides standalone spies, stubs and mocks and integrates very well with Jasmine and other JavaScript testing frameworks.


Listings

team-list.component.ts

import { Component, OnInit } from '@angular/core';
import { TeamService } from './team.service';
import { Team } from './team';
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'app-team-list',
  template: `
    <ul>
      <li *ngFor="let team of teams$ | async">
        {{team.name}}
      </li>
    </ul>`,
})
export class TeamListComponent implements OnInit {
  teams$: Observable<Team[]>;

  constructor(private teamService: TeamService) {
  }

  ngOnInit() {
    this.teams$ = this.teamService.getTeams();
  }
}

team.service.ts

import { Injectable } from '@angular/core';
import { Team } from './team';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class TeamService {

  constructor(private http: HttpClient) {
  }

  getTeams(): Observable<Team[]> {
    console.log('getTeams');
    return this.http.get<Team[]>('/api/teams');
  }
}

team-list.component.spec.ts

import * as sinon from 'sinon';
import { TeamListComponent } from './team-list.component';
import { TeamService } from './team.service';
import { of } from 'rxjs/observable/of';

describe('TeamListComponent', () => {
  let sut: TeamListComponent;
  let collaborator: TeamService;

  it('should get the teams (stub)', () => {
    // Arrange
    collaborator = new TeamService(undefined);
    sut = new TeamListComponent(collaborator);

    const data = [{name: 'Barça'}];
    collaborator.getTeams = () => of(data);

    // Act
    sut.ngOnInit();

    // Assert
    sut.teams$.subscribe((teams) => {
      expect(teams).toEqual(data);
    });

  });

  it('should get the teams (spy)', () => {
    // Arrange
    collaborator = new TeamService(undefined);
    sut = new TeamListComponent(collaborator);
    const spy = spyOn(collaborator, 'getTeams');

    // Act
    sut.ngOnInit();

    // Assert
    expect(spy).toHaveBeenCalled();

  });

  it('should get the teams (mock)', () => {
    /*
     Setup - data
     */
    collaborator = new TeamService(undefined);
    sut = new TeamListComponent(collaborator);
    const mock = sinon.mock(collaborator);

    // Setup - expectations
    mock.expects('getTeams').once();

    // Exercise
    sut.ngOnInit();

    // Verify
    mock.verify();

  });
});