Тести в Angular
У Angular для тестів використовуються два інструменти:
Jasmine – фреймворк для написання тестів, і
Karma – інструмент для запуску цих тестів в браузері.
Jasmine
методи Jasmine
describe(description, function)
– метод застосовується для угруповання взаємозалежних тестівbeforeEach(function)
– метод застосовується для призначення завдання, яке повинно виконуватися перед кожним тестомafterEach(function)
– метод застосовується для призначення завдання, яке повинно виконуватися після кожного тестомit(description, function)
– метод застосовується для виконання тестуexpect(value)
– метод застосовується для ідентифікації результату тестуtoBe(value)
– метод застосовується для завдання очікуваного значення тесту: метод порівнює результат із значеннямtoEqual(object)
– перевіряє, що результатом є той самий об’єкт, що і заданий значенняtoMatch(regexp)
– перевіряє, що результат відповідає заданому регулярному виразуtoBeDefined()
– перевіряє, що результат визначенийtoBeUndefined()
– перевіряє, що результат не визначенийtoBeNull()
– перевіряє, що результат дорівнює NulltoBeTruthy()
– перевіряє, що результат є квазіістіннимtoBeFalsy()
– перевіряє, що результат є квазіложнимtoContain(substring)
– перевіряє, що результат містить задану підрядокtoBeLessThan(value)
– перевіряє, що результат менше заданого значенняtoBeGreaterThan(value)
– перевіряє, що результат більше заданого значення
Клас TestBed і його методи
Клас TestBed
– відповідає за моделювання середовища додатку Angular для виконання тестів.
TestBed.configureTestingModule
– налаштовуємо тестовий модуль;TestBed.createComponent
– отримуємо екземпляр компонента;compileComponents
– застосовується для компіляції компонентів.compileComponents
повертаєPromise
, який дозволяє налаштувати основні змінні для тесту після компіляції компонента (наприклад, коли ми використовуємо зовнішній шаблон і т.д.);
beforeEach( async(() => { TestBed.configureTestingModule({ declarations:[TestComponent] }); TestBed.compileComponents().then(() => { fixture = TestBed.createComponent(TestComponent); component = fixture.componentInstance; }); }));
Зверніть увагу на метод async
, який займається тим, що відстежує всі асинхронні завдання всередині нього, приховуючи від нас складність асинхронного виконання.
ComponentFixture
Результатом методу createComponent
є об’єкт ComponentFixture
, який надає властивості і методи для тестування компонента:
componentInstance
– повертає об’єкт компонента;nativeElement
– повертає об’єкт DOM, який представляє керуючий елемент для компонента;debugElement
– повертає тестовий керуючий елемент для компонента;detectChanges()
– примушує тестову середу виявляти зміни стану і відображати їх в шаблоні компонента;whenStable()
– повертає об’єктPromise
, дозволяються при повній обробці всіх змін (використовується з асинхронність, наприклад, коли ви отримуєте дані асіхронно від будь-якої фейковий служби)
let fixture: ComponentFixture <MyComponent>>; let component: MyComponent; let dataSource = new MockDataSource(); beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MyComponent ], providers: [ { provide: RestDataSource, useValue: dataSource } ] }); TestBed.compileComponents().then(() => { fixture = TestBed.createComponent( MyComponent ); // debugElement = fixture.debugElement; component = fixture.componentInstance; }); }));
ComponentFixture.debugElement
Властивість ComponentFixture.debugElement
– повертає об’єкт типу DebugElement
(кореневог елементу шаблону компонента).
debugElement
має ряд властивостей і методів:
nativeElement
– повертає об’єкт, який представляє об’єкт HTML в DOM;children
– повертає масив ‘дочірніх’ об’єктів типуDebugElement
;query(selectorFunction)
– функція selectorFunction отримує в якості параметра об’єкт типуDebugElement
для кожного елемента HTML в шаблоні; функція повертає перший об’єкт типуDebugElement
, для якого функція поверне true;queryAll(selectorFunction)
– аналогічно query, але поверне всі об’єкти типуDebugElement
;triggerEventHandler(name, event)
– ініціюємо подію, наприклад, за допомогою декоратора@HostListener
;
Клас By
Клас By
дозволяє знаходити елементи в шаблоні компонента завдяки своїм методам:
By.all
By.css(selector)
By.directive(type)
Unit-тести: тестування сервісів
angular-cli
при створенні сервісу генерує файл виду name_service.spec.ts
як болванку для тесту. В даному файлі за допомогою хелперів TestBed
і inject
створюється наша сутність.
import { TestBed, inject } from '@ angular/core/testing';
TestBed
допомагає настроїти модуль, в якому ми повинні вказати provider
для нашого сервісу. inject
– це хелпер, який допомагає запустити injector
по заданому провайдеру, завдяки йому ми отримуємо примірник нашого сервісу (service: ArticleService)
.
Далі ми проводимо простий тест на те, що сервіс існує:
expect(service).toBeTruthy();
Повний код тесту, який спочатку згенерував angular-cli
:
import { TestBed, inject } from '@ angular/core/testing'; import { ArticleService } from './article.service'; describe('ArticleService', ()=>{ beforeEach(() =>{ TestBed.configureTestingModule({ providers: [ ArticleService ] }); }); it( 'should be created', inject([ArticleService], ( service: ArticleService ) => { expect(service).toBeTruthy>(); })); });
Припустимо, в сервісі у нас є метод sum
, який повертає результат складання двох чисел:
sum(a: number, b: number): number { return a + b; }
Протестуємо метод sum
:
it( 'should return sun', inject([ ArticleService ], ( service: ArticleService ) => { expect(service.sum( 5, 5)).toBe(10); }));
Щоб запустити тести необхідно виконати команду (ng test
в package.json):
npm run test
Працює з асинхронним методом в сервісі
Асинхронний метод в сервісі sum.service.ts
:
// async method sumAsync( a: number, b: number): Promise<any> { return new Promise(function(resolve, reject) { setTimeout(function(){ resolve( a + b); }, 500 ); }); }
Тестуємо в sum.service.spec.ts
:
Для емуляції асинхронности в Angular присутній метод fakeAsync
. Також для емуляції асинхронности присутні два додаткових методи, які ми використовуємо всередині методу fakeAsync
: flush
(flush
всередині fakeAsync
скине всі асинхронности, тобто виконає їх синхронно), tick
– приймає параметр (к-ть мілісекунд), після закінчення яких виконається код в методі fakeAsync
.
// test async method it('should return async sum', fakeAsync(inject([ SumService ], ( service: SumService ) => { service.SumAsync(5, 5).Then( function(data){ // повернутий результат ми можемо затверджувати: expect(data).toBe(10);}); // тести пройшли і якщо випаде помилка, так як виконується асинхронна операція ніхто чекати не буде // можна скористатися методом flush(). flush всередині fakeAsync скине всі асинхронности, тобто виконає їх синхронно. // flush (); // OR: // або ми можемо скористатися всередині fakeAsync методом tick, в який можна передати кількість мілісекунд tick>(500); })));
Метод whenStable
Метод whenStable
повертає об’єкт Promise
, дозволяється при повній обробці всіх змін (використовується з асинхронність, наприклад, коли ви отримуєте дані асіхронно від будь-якої фейковий служби). Наприклад, це дає нам можливість дочекатися виконання Observable
і лише потім протестувати наші зміни.
@Injectable() class MockDataSource { public data = [ { item: 1, item: 2 } ]; getData(): Observable { return new Observable( obs => { setTimeout(() => obs.next(this.data), 500); }) } } describe("TestComponent", ()=>{ let fixture: ComponentFixture; let component: TestComponent; // створюємо фіктивну службу, яка для тесту підміняє службу компонента let dataSource = new MockDataSource(); beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ TestComponent ], providers: [ { provide: RestDataSource, useValue: dataSource } ] }); TestBed.compileComponents().then(() => { fixture = TestBed.createComponent( TestComponent ); component = fixture.componentInstance; }); })); it("async operation success", () => { fixture.detectChanges(); fixture.whenStable().then(() => { expect(component.getItems().length).toBe(2); }); }); });
Unit-тести: тестування httpClient
Розглянемо як тестувати сервіси, які містять http-запити.
Щоб тестувати http-запити необхідно також налаштувати proxy. У файлі karma.conf.js вкажіть параметр proxies
:
proxies: { '/api': { 'target': 'http://localhost:3000/api', 'secure': false, 'changeOrigin': true, 'logLevel': 'info'} }
Якщо консоль буде майоріти помилками виду StaticInjectorError(DynamicTestModule)[HttpClient]
, то це означає, що в компоненті використовуються http-запити і необхідно імпортувати HttpClientModule
:
imports: [ HttpClientModule, ],
@Injectable ({ providedIn: 'root' }) export class CatsService{ constructor(private httpClient: HttpClient){ } getCats(): Observable<any> { return this.httpClient.get('/api/cats'); } getCat(_id): Observable<any> { return this.httpClient.get(`/api/cat/${_ id}`); } }
Протестуємо запит на отримання cat
по id
.
import { TestBed, inject } from '@angular/core/testing'; import { CatsService } from './cats.service'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; describe('CatsService', () => { // HttpClientTestingModule - // HttpTestingController - // допомогою HttpClientTestingModule і HttpTestingController ми можемо управляти http-запитами beforeEach(() => { TestBed.configureTestingModule ({ imports: [ HttpClientTestingModule ], providers: [ CatsService ] }); }); it('should be created', inject([ CatsService ], (service: CatsService) => { expect(service).toBeTruthy(); })); // так як ми робимо unit-тести, то ми не тестуємо сам httpClient // (він протестований розробниками Angular) it('should get one cat', inject([CatsService, HttpTestingController], (service: CatsService, backend: HttpTestingController) => { // створимо фейковий об'єкт cat (він приходить з сервера) const mockCat = { name: 'asdasd' }; service.getCat('5b22e86df062b20530788e2d').subscribe(function(cat) { // так як приходить об'єкт, то для порівняння використовуємо toEqual expect(cat).toEqual(mockCat); }); // сформуємо сервіс backend, щоб він повертав нам потрібні дані backend.expectOne({ method: 'Get', url: '/api/cat/5b22e86df062b20530788e2d' }).flush(mockCat) // flush дозволяє скинути і вказати, що повинен повернути запит })); >});
Unit-тести: тестування pipe
Створимо pipe
, який буде перевертати рядок – reverse-string
.
@Pipe ({ name: 'reverseString' }) export class ReverseStringPipe implements PipeTransform { transform (value: string): string { if(typeof value !== 'string') { throw new Error('Error on Reverse: not string') } let str = ''; for(let i = value.length - 1; i >= 0; i-){ str += value.charAt(i); } Return str; } }
Фільтри ми можемо тестувати як звичайний js-код, тобто тестувати клас, а в цьому класі тестувати метод.
import { ReverseStringPipe } from './reverse-string.pipe'; describe('ReverseStringPipe', () => { let pipe; beforeEach(() => { pipe = new ReverseStringPipe(); }); // тест на те, що pipe існує it('create an instance', () => { expect(pipe).ToBeTruthy(); }); // тест на те, щo рядок 'перевертається' it('reverse success', () => { expect(pipe.Transform('abcde')).ToBeTruthy('edcba'); }); // * перевіряємо на те, що виключення (exception) було викинуто it('should throw on error', () => { // всередині expect exception потрібно звернути в функцію expect(() => { pipe.Transform(1212); }).toThrowError('Error on Reverse: not string'); }); });
Хелпери beforeEach
Хелпер beforeEach
дозволяє задати код, який буде виконаний перед кожним тестом. Наприклад, щоб кожен раз не створювати екземпляр pipe
(дивіться приклад – Unit-тести: тестування pipe
) ми можемо винести його в beforeEach
:
describe('ReverseStringPipe', () => { let pipe; beforeEach(() => { pipe = new ReverseStringPipe(); }); });
Unit-тести: Тестуємо компонент
У компоненті ми будемо отримувати за допомогою методу компонента getCat
і сервісу по id
певну cat
і виводити cat.name
в шаблоні. Ми повинні перевірити, що в методі викликається сервіс і то що значення записується в змінну компонента cat
.
// cat-info.component.ts import { Component, OnInit } from '@angular/core'; import { CatsService } from '../../services/cats.service'; @Component ({ selector: 'app-cat-info', templateUrl: './cat-info.component.html', styleUrls: [ './cat-info.component.css' ] }) export class CatInfoComponent implements OnInit { public cat: any; constructor(private catService: CatsService) { } ngOnInit() { this.getCat(); } getCat() { let self = this; self.catService.getCat("5b22e86df062b20530788e2d").subscribe(function(data) { self.cat = data; }); } }
// cat-info.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CatInfoComponent } from './cat-info.component';
// імпортуємо так як в компоненті використовуємо запити до апі
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CatsService } from "../../services/cats.service";
import { of } from "rxjs";
describe('CatInfoComponent', () => {
let component: CatInfoComponent;
let fixture: ComponentFixture <CatInfoComponent>;
let catService: CatsService;
let spy: jasmine. Spy;
let mockCat;
beforeEach(async(() => { TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ],
declarations: [ CatInfoComponent ],
providers: [ CatsService ]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CatInfoComponent);
// instance компонента
component = fixture.componentInstance;
// injector - це сутність, яка дозволяє створювати залежності для компонента
// (ми отримуємо injector для нашого конкретного компонента) . Отримуємо CatsService:
catService = fixture.debugElement.injector.get(CatsService);
mockCat = {
Name: "asas"
};
// нам потрібен шпигун на метод getCat; також повернемо значення, яке повертає метод getCat
spy = spyOn(catService, 'getCat').and.returnValue(of(mockCat))
fixture.detectChanges();
});
// перевіряємо на існування компонента
it('should create', () => {
expect (component ).ToBeTruthy();
});
// перевіримо що метод сервісу викликається
// для цього нам будуть потрібні спеціальні суті - шпигуни
// (jasmine spy, https://jasmine.github.io/api/2.9/Spy.html).
// Коли ви створюєте сутність spy, то вона буде зберігати всі виклики
// і звернення до цього spy, також вона може повертати будь-які
// значення замість реальної суті.
it('should call catService.getCat', () => {
// шпигун був викликаний як завгодно раз
expect(spy.calls.any()).toBeTruthy();
});
// перевіримо що сервіс записує значення після виклику сервісу
it('should set cat(cat.name)', () => {
expect(component.Cat.Name).ToEqual('asas');
});
});
Unit-тести: Тестуємо директиву
Наша директива Emit (передає наверх) батьківського компоненту кількість кліків.
// count-click.directive.ts @Directive({ selector: '[appCountClick]'}) export class CountClickDirective { @Output() changes = new EventEmitter<number>(); private count = 0; @HostListener('click') onClick() { this.count++; this.changes.emit(this.count); } }
Використання директиви:
<div appCountClick(changes) = "getClick ($ event)"> click me </ div>
Для директиви Angular в заготівки робить екземпляр від класу директиви; цього мало, але так як директиви дуже різнобічні розробники Angular вирішили, що цього достатньо. Нам необхідно протестувати роботу декораторів @Output
і @HostListener
, для цього потрібно навісити кудись подію і висновок. Тому створимо тестовий компонент в файлі count-click.directive.spec.ts:
// count-click.directive.spec.ts // тестовий компонент @Component({ template: `<div appCountClick (changes) = "outCount = $event"> click me </ div> ` }) export class TestCountComponent { public outCount = 0; } describe('CountClickDirective', () => { let testCountComponent, fixture; // инициализируем тестовий компонент beforeEach(() => { // налаштуємо модуль TestBed.ConfigureTestingModule({ declarations: [ TestCountComponent, CountClickDirective ] }); fixture = TestBed.createComponent(TestCountComponent); // отримаємо екземпляр компонента testCountComponent = fixture.componentInstance; }) it('should create an instance', () => { const directive = new CountClickDirective(); expect(directive).toBeTruthy(); }); // перевіримо кліки по диву it('should count click', () => { // отримуємо div, для якого ми ставимо директиву appCountClick let div = fixture.NativeElement.QuerySelector('div'); div.Click(); expect(testCountComponent.outCount).toBe(1); }); });