My summary sheet of unit tests in Angular

For those of you who have had the opportunity to look at the Angular framework (V2 and more) you have seen how the documentation on the official site could be voluminous, especially on the Unit Tests part. The purpose of this article is to provide you with a summary sheet of unit testing best practices in Angular.
The article assumes that you have already used Angular and its unit tests, and that you know the lexical field of Unit Tests.
The advice and solutions given here come from a six-month experience of Angular development with a team of five people. They are of course subject to improvement/discussions.

Preamble: How to “mock” correctly with Angular?

A small preamble is necessary to clarify a point which for me is too often overlooked in the literature on this subject, and which will be vital for the rest of the understanding:
Mocks, Spy and other Stub.
Too often, a somewhat naïve approach is offered in Angular unit test examples. Let's take the case from the test of a component in the official doc:

class MockUserService {
  isLoggedIn = true;
  user = { name: "Test User" };
}
/* ... */
beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.get(WelcomeComponent);
  userService = TestBed.get(UserService);
});

This code explains that to mock the implementation of a service (here UserService) just simply create a fake class replacing this service (here MockUserService).
This approach basically annoys me because we lose all type checking. In fact, there is no guarantee that MockUserService respects the interface of the class UserService. Added to that is the cost of writing a fake class...
[bctt tweet=”In Angular, creating a mock by hand is neither a safe nor an efficient approach.”] Another way to do it, which can be seen here and there, is to extend the class that we want to mock then, overload the desired behaviors. If we go back to our previous example, it would look like this:

class MockUserService extends UserService {
  user = { name: "Another user" };
}
/* ... */
beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      FirstLevelNeededService,
      SecondLevelNeededService,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.get(WelcomeComponent);
  userService = TestBed.get(UserService);
});

This method is even worse than the previous one because even though you fixed the typing and verbosity problem, you broke the isolation needed for a unit test.
Like the real implementation of UserService is imported, TestBed will require that we also provide it with all the classes on which UserService. Imagine the dependency schema below:

+---------------+ +------------+ +---- ----------------------+ | | | | | | | UserService +<---+ FirstLevelNeededService +<----+ SecondLevelNeededService | | | | | | | +---------------+ +------------+ +---- ----------------------+

Following this scheme, we should provide the TestBed, FirstLevelNeededService et SecondLevelNeededService. This doesn't make sense when you think about it because the purpose of a mock is to simulate the behavior of a class. So why provide mock dependencies?
[bctt tweet=”Never create a mock of a TypeScript class by extending its original implementation in Angular.”] Otherwise you will have to provide a large number of classes for each test suite.

Use a mock library

For Object-oriented developers, using a mock library is obvious in the context of unit testing because their language is often not as dynamic as JavaScript and does not allow you to write Mocks on the fly ("on the fly"). snatch?” – Who said that?).
A Mock library will use the type definition created via TypeScript to create a mock of a class.
Old reflex of “Javaiste” perhaps, I personally like ts mockito as Mock's bookseller, but there are others: TS-mock or even Typemoq
If we take our example with ts-mockito, our code would look like this:

import { instance, mock, when } from "ts-mockito";
/***/
let userServiceMock: UserService;
beforeEach(() => {
  userServiceMock = mock(UserService);
  when(userServiceMock.user).thenReturn({ name: "A user" });
  TestBed.configureTestingModule({
    providers: [
      WelcomeComponent,
      { provide: UserService, useValue: instance(userServiceMock) }
    ]
  });
  userService = TestBed.get(UserService);
});

Simple, isn't it? ts-mockito will first create a mocked class using the function mock then create mock instances based on this class with the function instance.
To simulate results, we will often use the function when. Always use when on a mocked class and call instance only afterwards, otherwise your mock instance will not have the defined behaviors.

In the rest of the article we will systematically use ts-mockito

The guide

Case n°1: Testing a service without dependency with Angular

Let's start with the simplest, the service. In Angular, a service is neither more nor less than a TypeScript class that will be instantiated by the IOC-container of Angular and injected into all other elements that will define it as dependencies.
If the service does not use a dependency coming from Angular (like the HttpClient for example) you can test it like any TypeScript class in any project like this:

import { MyService } from "./my.service";
import { instance, mock, when } from "ts-mockito";
import { MathLib } from "./my.service";
describe("MyService", () => {
  it("should add correctly two positive integers and multiply them by two", () => {
    // given
    const mathMock = mock(MathLib);
    when(mathMock.add(1, 2)).thenReturn(3);
    when(mathMock.multiply(3)).thenReturn(6);
    const myService = new MyService(instance(mathMock));
    // when
    const result = myService.addAndMultiplyByTwo(1, 2);
    // then
    expect(result).toBe(6);
  });
});

First point, you will notice that contrary to angular-cli we do not create a test should be created. This test which would be used to verify the existence of the class is de facto already verified by the other unit tests, therefore useless.
Second and potentially most important point of this article:
[bctt tweet=”Use the `TestBed` only rarely in Angular unit testing.”] The TestBed, more specifically the call to its method configureTestingModule, is long and brings more complexity in the tests. And for good reason, the method call TestBed.configureTestingModule will, as its name suggests, create a Angularmodule at each call and instantiate all the elements of this 'fake module' thanks to the IOC container of Angular.
We have observed in our tests that the use of TestBed will potentially take five times longer than a simple object instantiation and its mocks as in our example (more info here): Angular TestBed is too long)
We didn't invent anything there, it's called a Isolated Test in the Angular doc (which I can't find the link anymore because it's huge :D)
Ok, but what happens if I use Module coming from Angular such as the HttpModule ?

Case n°2: Testing a service that depends on an Angular module

Let's take a fairly standard test of an authentication service that uses the HttpClient :

import { AuthService } from "./auth.service";
import { inject, TestBed } from "@angular/core/testing";
import {
  HttpClientTestingModule,
  HttpTestingController
} from "@angular/common/http/testing";
describe("AuthService", () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [AuthService],
      imports: [HttpClientTestingModule]
    });
  });
  it(
    "should return null user when server responds HTTP error",
    inject(
      [HttpTestingController, AuthService],
      (httpMock: HttpTestingController, service: AuthService) => {
        service.user.subscribe(user => {
          expect(user).toBe(null);
        });
        httpMock.expectOne("auth/user").error(new ErrorEvent("401"));
        httpMock.verify();
      }
    )
  );
});

We have no choice but to use the TestBed because it is he who allows access to the httpMock responsible for simulating server responses.
[bctt tweet=”We will only use the `TestBed` to test components or classes dependent on Angular module.”] You will also notice the use of the small helper inject, handy for retrieving instances created inside Angular's IOC container. You may also have noticed that my beforeEach does not contain a call to async. Async tends to be used indiscriminately, but this function is there to wait for an asynchrony.
If you look at the official documentation configureTestingModule do not return from Promise, so it's not asynchronous, so it's not worth calling async in that case.

Case n°3: Testing a “basic” component

This is the kind of component you write a lot in Angular.
We will see in the following case that the components located high in the hierarchy will also be the subject of additional processing.
To illustrate this test, we will imagine being in the process of checking a component ListComponent which displays a list loaded from a service ItemsService : a textbook case…
The correct way to do it would look like this:

import { ComponentFixture, TestBed } from "@angular/core/testing";
import { anyString, instance, mock, when } from "ts-mockito";
import { ListComponent } from "./list.component";
import { ItemsService } from "./item/items.service";
import { Item } from "./item/item.model";
import { of } from "rxjs/observable/of";
describe("ListComponent", () => {
  let component: ListComponent;
  let fixture: ComponentFixture<ListComponent>;
  let mockedItemsService: ItemsService;
  beforeEach(() => {
    mockedItemsService = mock(ItemsService);
  });
  async function configureTestingModule() {
    await TestBed.configureTestingModule({
      providers: [
        { provide: ItemsService, useValue: instance(mockedItemsService) }
      ],
      declarations: [ListComponent]
    }).compileComponents();
    fixture = TestBed.createComponent(ListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  }
  it("should display an empty list", async () => {
    // given
    when(mockedItemsService.loadItems()).thenReturn(of([new Item()]));
    // when
    await configureTestingModule();
    // then
    const nbEl = fixture.debugElement.queryAll(By.css("li")).length;
    expect(nbEl).toEqual(0);
  });
});

The first thing that might have shocked you was the function configureTestingModule outside of beforeEach. Indeed, it will be necessary to call it in our unit tests and not before in order to maintain the call order of the mockito functions mentioned in the preamble:

mock -> when -> instance

mock will be called in the beforeEach and when at the beginning of the unit test and finally instance through the call of configureTestingModule in the middle of unit testing.
If we focus on configureTestingModule, we see that it is asynchronous because we invoke compileComponents which returns a promise. This async “contaminates” our unit test because we have to mark it as async also.
We will also notice that we are overloading the provider with ItemsService to give it the instance of our ts-mockito mock:

{ provide: ItemsService, useValue: instance(mockedItemsService) }

This will ensure that the TestBed injects this simulacrum of ItemsService to the component when it is created.
Finally, we simulate the return of the method loadItems of the service so that it returns a Observable&lt;Item[]&gt; empty and we check that the html list of elements is indeed empty.

Case n°4: Testing a high-level component

What I call “high-level” components, are the components that are high up in our application's component hierarchy. Components pages or containing many children correspond perfectly to this denomination. the AppComponent will be the example used.
The annoying thing to check the behavior of this kind of component: they depend by definition on many sub-components and sub-modules and it will be necessary TOUS add them in the TestBed. For the AppComponent it's like giving him almost the whole app in dependency...
If, moreover, I told you that the TestBed.configureTestingModule was slow in case 1, it gets slower as the number of dependencies increases.
To overcome this problem, there is the parameter schemas to be provided to TestBed.configureTestingModule with the value NO_ERRORS_SCHEMA :

import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
import { NO_ERRORS_SCHEMA } from "@angular/core";
describe("AppComponent", () => {
  let component: ListComponent;
  let fixture: ComponentFixture<ListComponent>;
  beforeEach(async () => {
    TestBed.configureTestingModule({
      providers: [],
      imports: [],
      declarations: [AppComponent],
      schemas: [NO_ERRORS_SCHEMA]
    }).compileComponents();
    fixture = TestBed.createComponent(ListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  it("should render title in a h1 tag", async () => {
    // given
    const selectorText = compiled.querySelector("h1").textContent;
    // then
    expect(selectorText).toBe("My awesome app");
  });
});

This magic parameter, configures the TestBed so that it does not throw an error if it cannot find the provider of a component or a module used in the template(s) of the component(s) tested. This method is called shallow testing. We only test the component AppComponent and do not interpret its subcomponents.
Le NO_ERRORS_SCHEMA is to be used with caution, because applied everywhere, it renders dependency system checks inert and could cause you to forget to load a component when it is necessary in your test case.
[bctt tweet=”In Angular, `NO_ERRORS_SCHEMA` should be used with caution and only for testing components high in the hierarchy.”]

Case n°5: Pipes, classes, the rest

The pipes, like the rest of the elements that could compose your application, will be tested like services without Angular dependency, namely basic TypeScript classes.
Let's imagine a pipe whose goal is to join in the form of a character string all the keys of an object whose value is not null. We would call it AliciaKeys… (yes, we were quite proud of the name)

import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
  name: "aliciaKeys"
})
export class AliciaKeys implements PipeTransform {
  transform(obj: any, arg: string[]): string {
    return obj
      ? Object.entries(obj)
          .filter(([key, value]) => !!value)
          .map(([key]) => key)
          .join(", ")
      : "";
  }
}

The attached unit test file is trivial since it is a simple instantiation of the class AliciaKeys with two calls from transform in two different cases:

import { AliciaKeys } from "./aliciakeys.pipe";
describe("AliciaKeys", () => {
  it("should return valued keys", () => {
    const result = new AliciaKeys().transform({
      validKey: {},
      inValidKey: null,
      anotherValidKey: 1
    });
    expect(result).toEqual("validKey, anotherValidKey");
  });
  it("should return blank when input is falsy", () => {
    const result = new AliciaKeys().transform(null;
    expect(result).toEqual("");
  });
});

Conclusion & going further

We hope that each case description will help you, too, to better understand how to properly unit test your Angular applications.
The important points to remember are that the classes in Angular, remain classic TypeScript classes and that they can be tested easily. The second point is that the TestBed is not systematic, that it should be used only when you have no choice. Finally, mocks in Angular must be managed with a mock library: In our case ts-mockito.
You should know that in our project, we had about 700 unit tests that took more than 30 seconds to run. Streamlining tests and speeding them up became crucial for us.
We have also continued this quest for acceleration by passing these last Jest. This is entirely possible by following the instructions in the repo jest-preset-angular and it saves a lot of time!
We will come back to you with more details on these subjects during our “JS-Talks” feedback (Technology watch day that we do every month). Some final photos to see what it looks like:

Published by Matthew Breton CTO at JS-Republic