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<Item[]>
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 Mathieu Breton CTO at JS-Republic