
With the unit tests, we can make sure that the individual parts of the application work exactly as we expect from them.
This, to some extent, saves the existing code from breakdowns, helps to clarify how it will work in certain cases. And, finally, it allows you to look at the code, so to speak, from the side, in order to see its weak points.
There is even an opinion that the code being difficult to test is a contender for rewriting.
')
The purpose of this article is to help in writing unit tests for Angular 5+ applications. Let it be a fascinating process, not a headache.
Isolated or Angular Test Bed?
As for the unit testing Angular applications, we can distinguish two types of tests:
- Isolated - those that do not depend on Angular. They are easier to write, easier to read and maintain, since they eliminate all dependencies. This approach is good for services and paypov.
- Angular Test Bed - tests, in which the TestBed test utility is used to set up and initialize the environment for testing. The utility contains methods that facilitate the testing process. For example, we can check whether a component has been created, how it interacts with the template, with other components and with dependencies.
Isolated
With an isolated approach, we test the service as an ordinary class with methods.
First we create an instance of the class, and then we check how it works in various situations.
Before proceeding to the examples, it is necessary to indicate that I am writing and running tests with
jest , because I liked its speed. If you prefer
karma +
jasmine , then the examples for you will also be relevant, since there are very few differences in syntax.
Jasmine / jest differencesjasmine.createSpy ('name') -> jest.fn ()
and.returnValue () -> mockReturnValue ()
spyOn (...). and.callFake (() => {}) -> jest.spyOn (...). mockImplementation (() => {})
Consider an example of a service for a modal window. It has only two methods that should send a specific value to the popupDialog variable. And absolutely no dependencies.
import { Injectable } from '@angular/core'; import { ReplaySubject } from 'rxjs/ReplaySubject'; @Injectable() export class PopupService { private popupDialog = new ReplaySubject<{popupEvent: string, component?, options?: {}}>(); public popupDialog$ = this.popupDialog.asObservable(); open(component, options?: {}) { this.popupDialog.next({popupEvent: 'open', component: component, options: options}); } close() { this.popupDialog.next({popupEvent: 'close'}); } }
When writing tests, do not forget about the order of execution of the code. For example, the actions that must be performed before each test, we put in beforeEach.
So, we will need an instance of the service created in the code below for each check.
import { PopupService } from './popup.service'; import { SignInComponent } from '../components/signin/signin.component'; describe('PopupService', () => { let service: PopupService; // PopupService beforeEach(() => { service = new PopupService(); }); // done , it('subscribe for opening works', (done: DoneFn) => { // open service.open(SignInComponent, [{title: ' ', message: ''}]); // popupDialog$ subscribe service.popupDialog$.subscribe((data) => { expect(data.popupEvent).toBe('open'); done(); }); }); it('subscribe for closing works', (done: DoneFn) => { service.close(); service.popupDialog$.subscribe((data) => { expect(data.popupEvent).toBe('close'); done(); }); }); });
Angular Test Bed tests
Simple component
And now let's take a look at the full power of the TestBed utility. As an example, let's start with the simplest component:
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; }
Template file:
<h1> Welcome to {{ title }}! </h1>
The test file will be disassembled in pieces. To begin with, set the TestBed configuration:
beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], }).compileComponents()
compileComponents is a method that makes styles and template rendered in separate files embedded.
This process is asynchronous, because the Angular compiler must retrieve data from the file system.
Sometimes compileComponents are not needed.If you use WebPack, then you do not need this call and the async method.
The fact is that WebPack automatically embeds external styles and a template before running tests.
Accordingly, when writing styles and templates inside a component file, you do not need to compile yourself.
For tests, it is necessary for the components to be compiled before their instances are created through the createComponent () method.
Therefore, we placed the body of the first BeforeEach in asyn method, so that its contents are executed in a special asynchronous environment. And until the compileComponents () method is executed, the following BeforeEach will not start:
beforeEach(() => { fixture = TestBed.createComponent(AppComponent)
Due to the removal of all common data in beforeEach, the further code is much cleaner.
First, let's check the creation of the component instance and its property:
it('should create the comp', => { expect(comp).toBeTruthy(); }); it(`should have as title 'app'`, () => { expect(comp.title).toEqual('app'); });
Next, we want to check that the variable of the title component is inserted into the DOM. At the same time, we expect that it is assigned the value of 'app'. And this assignment occurs when the component is initialized.
By running the loop with the detectChanges CD, we initialize the component.
Prior to this call, the connection of DOM and component data will not occur, and therefore tests will not pass.
it('should render title in a h1 tag', () => { fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent) .toContain('Welcome to app!'); });
Full component test code import { TestBed, async, ComponentFixture } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { let comp: AppComponent; let fixture: ComponentFixture<AppComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AppComponent); comp = fixture.componentInstance; }); it('should create the comp', () => { expect(comp).toBeTruthy(); }); it(`should have as title 'app'`, () => { expect(comp.title).toEqual('app'); }); it('should render title in a h1 tag', () => { fixture.detectChanges(); const compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent) .toContain('Welcome to app!'); }); });
Component with dependencies
Let's complicate our component by incorporating a service into it:
export class AppComponent { constructor(private popup: PopupService) { } title = 'app'; }
It seems to be not particularly complicated, but the tests will not pass. Even if you didn’t forget to add the service to the AppModule providers.
Because these changes should also be reflected in TestBed:
TestBed.configureTestingModule({ declarations: [ AppComponent ], providers: [PopupService] });
We can specify the service itself, but it is usually better to replace it with a class or an object that describes exactly what we need for the tests.
Why?And you imagine a service with a bunch of dependencies and you have to prescribe everything during testing. Not to mention the fact that we are testing the component in this case. In general, testing one thing is just about unit tests.
So, we write stub as follows:
const popupServiceStub = { open: () => {} };
Methods we set only those which we test.
If we want to describe stub as a class class popupServiceStub { open() {} }
providers: [{provide: PopupService, useClass: popupServiceStub } ]
We add providers to TestBed configuration:
providers: [{provide: PopupService, useValue: popupServiceStub } ]
Do not confuse PopupService and PopupServiceStab. These are different objects: the first is a clone of the second.
Great, but we introduced the service not just like that, but for use:
ngOnInit() { this.popup.open(SignInComponent); }
Now you have to make sure that the method is actually called. To do this, first get a copy of the service.
Since in this case the service is defined in the root module providers, we can do this:
popup = TestBed.get(PopupService);
How else?If it were a question of a service that is registered in the component providers, you would have to receive it like this:
popup = fixture.debugElement.injector.get(PopupService);
Finally, the test itself:
it('should called open', () => { const openSpy = jest.spyOn(popup, 'open')
Our actions:
- Install the spy on the open method of the popup object.
- Run the CD loop, during which ngOnInit is executed with the method being tested.
- Make sure he was called.
Notice that we are checking exactly the call to the service method, and not what it returns or other things related to the service itself. They
, in order to preserve sanity, should be tested in service.
Service with http
More recently (in Angular 4), the service test file with queries could look truly intimidating.
Remember how it was beforeEach(() => TestBed.configureTestingModule({ imports: [HttpModule], providers: [ MockBackend, BaseRequestOptions, { provide: Http, useFactory: (backend, defaultOptions) => new Http(backend, defaultOptions), deps: [MockBackend, BaseRequestOptions] }, UserService ] }))
However, even now the Internet is full of articles with these examples.
Meanwhile, the Angular developers were not idle, and we can now write tests much easier. Just using
HttpClientTestingModule and
HttpTestingController .
Consider the service:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { ReplaySubject } from 'rxjs/ReplaySubject'; import { Game } from '../models/gameModel'; import { StatisticsService } from './statistics.service'; @Injectable() export class GameService { gameData: Array<Game>; dataChange: ReplaySubject<any>; gamesUrl = 'https://any.com/games'; constructor(private http: HttpClient, private statisticsService: StatisticsService) { this.dataChange = new ReplaySubject(); } getGames() { this.makeResponse() .subscribe((games: Array<Game>) => { this.handleGameData(games); }); } makeResponse(): Observable<any> { return this.http.get(this.gamesUrl); } handleGameData(games) { this.gameData = games; this.doNext(games); this.statisticsService.send(); } doNext(value) { this.dataChange.next(value); } }
To begin with, we describe all of our global heroes:
let http: HttpTestingController; let service: GameService; let statisticsService: StatisticsService; const statisticsServiceStub = { send: () => {} };
Here from interesting - stab statisticsService. We, by analogy with the component, will stabilize dependencies, as we are testing only specific services now.
As you can see, I just prescribed exactly what is needed in this test. Just imagine that StatisticsService is actually a huge number of methods and dependencies, and we use only one method in this service.
Next, we will announce the data that we will throw in response to the request:
const expectedData = [ {id: '1', name: 'FirstGame', locale: 'ru', type: '2'}, {id: '2', name: 'SecondGame', locale: 'ru', type: '3'}, {id: '3', name: 'LastGame', locale: 'en', type: '1'}, ];
In TestBed, you must import the HttpClientTestingModule and register all services:
TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, ], providers: [GameService, { provide: StatisticsService, useValue: statisticsServiceStub }] });
The next step is to get instances of all the services we need:
service = TestBed.get(GameService); statisticsService = TestBed.get(StatisticsService); http = TestBed.get(HttpTestingController);
It doesn’t hurt to immediately check in afterEach that there are no pending requests:
afterEach(() => { http.verify(); });
And go to the tests themselves. The simplest thing we can check is whether the service has been created. If you forget to specify any dependency in TestBed, then this test will not pass:
it('should be created', () => { expect(service).toBeTruthy()
Then it is more interesting - we check that, with the expected request, we will receive certain data, which we also throw in:
it('should have made one request to GET data from expected URL', () => { service.makeResponse().subscribe((data) => { expect(data).toEqual(expectedData)
It doesn’t hurt to check the ReplaySubject how it works, that is, whether the received games will be tracked by subscribers:
it('getGames should emits gameData', () => { service.getGames(); service.dataChange.subscribe((data) => { expect(data).toEqual(expectedData); }); const req = http.expectOne(service.gamesUrl); req.flush(expectedData); });
Finally, the last example is checking that the statisticsService send method will be called:
it('statistics should be sent', () => { const statisticsSpy = jest.spyOn(statisticsService, 'send')
Full test code import { TestBed } from '@angular/core/testing'; import { GameService } from './game.service'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { StatisticsService } from './statistics.service'; import 'rxjs/add/observable/of'; describe('GameService', () => { let http: HttpTestingController; let service: GameService; let statisticsService: StatisticsService; const statisticsServiceStub = { send: () => {} }; const expectedData = [ {id: '1', name: 'FirstGame', locale: 'ru', type: '2'}, {id: '2', name: 'SecondGame', locale: 'ru', type: '3'}, {id: '3', name: 'LastGame', locale: 'en', type: '1'}, ]; beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, ], providers: [GameService, { provide: StatisticsService, useValue: statisticsServiceStub }] }); service = TestBed.get(GameService); statisticsService = TestBed.get(StatisticsService); http = TestBed.get(HttpTestingController); }); afterEach(() => { http.verify(); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should have made one request to GET data from expected URL', () => { service.makeResponse().subscribe((data) => { expect(data).toEqual(expectedData); }); const req = http.expectOne(service.gamesUrl); expect(req.request.method).toEqual('GET'); req.flush(expectedData); }); it('getGames should emits gameData', () => { service.getGames(); service.dataChange.subscribe((data) => { expect(data).toEqual(expectedData); }); const req = http.expectOne(service.gamesUrl); req.flush(expectedData); }); it('statistics should be sent', () => { const statisticsSpy = jest.spyOn(statisticsService, 'send'); service.handleGameData(expectedData); expect(statisticsSpy).toHaveBeenCalled(); }); });
How to facilitate testing?
- Choose the type of tests that is appropriate in this situation and do not forget about the essence of the unit tests.
- Make sure you know all the capabilities of your IDE in testing assistance.
- When generating entities using Angular-cli, the test file is automatically generated
- If a component has many dependencies such as directives and child components, then you can disable checking their definitions. To do this, we set up the NO_ERRORS_SCHEMA in the TestBed configuration:
TestBed.configureTestingModule({ declarations: [ AppComponent ], schemas: [ NO_ERRORS_SCHEMA ] })
Without epilogue can not do
To cover all the points in one article, so that it does not become frightening (a la
documentation ), it is quite difficult. But it seems to me that the main thing is to understand what tools you have and what to do with them, and then you will fearlessly come across in practice both banal and non-trivial cases.
If after reading the article you have become something a little clearer - hurray!
Do you have anything to add? Do you disagree with something?
Well, maybe, for the sake of your valuable comments, this article was started.
PS Oh yes, here is a link to all the
examples .
Also for all those interested in Angular, the Russian Angular
community in a telegram can be useful.