Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В прошлой статье, я описывал как тестировать компонент. Теперь же коснемся вопроса тестирования сервиса.
Вступление
В данном примере будем использовать так же, инструменты идущие из "коробки", такие как Karma и Jasmine.
В кратце повторю что это за инструменты: Karma это так называемый тест раннер который позволяет нам настраивать то на каких устройствах или браузерах будут запускаться тесты и какие тестовые фреймворки и плагины, будут участвовать в тестах. Jasmine это BDD фрейворк для тестирования javascript кода.
Начальное приложение
В это раз у нас также есть некое стартовое приложение с созданным компонентом Comments и сервисом с одноименным названием. В корне нашего приложения, все также есть предсозданный файл karma.conf.js (создается автоматически при генерации приложения через Angular CLI). В нем находится конфиг нашего тест раннера. Здесь задаются настройки того, в каком браузере будет запускатся окно тест раннера (по умолчанию стоит Chrome), какие плагины подключать, и на каком порту запускать тест раннер (по умолчанию порт 9876). В src/app
лежит наш компонент Comments, который выглядит следующим образом:
import { Component, OnInit } from '@angular/core';
import { CommentsService } from '../shared/comments.service';
@Component({
selector: 'app-comments',
templateUrl: './comments.component.html',
styleUrls: ['./comments.component.scss']
})
export class CommentsComponent implements OnInit {
comments: any[] = [];
constructor(private service: CommentsService) {}
ngOnInit(): void {
this.service.getComments().subscribe(c => {
this.comments = c;
});
}
add(text: string) {
const comment = {text};
this.service.create(comment).subscribe( c => {
this.comments.push(c);
}, err => this.message = err);
}
}
Наш компонент "инжектирует" в конструктор, сервис CommentService
а также создает переменную comments
которая является массивом в который будут помещаться все наши комментарии. При инициализации компонента, в шаге ngOnInit
вызывается из сервиса, метод getComments
, на результат которого мы подписывается и кладем в comments
, результат вызова.
Так же присутствует метод add
, который принимает в себя текст комментария и вызывает внутри себя метод create
из сервиса, с помощью которого создает новый комментарий и передает его в массив comments
.
Рассмотрим сервис CommentsService
который лежит в папке shared:
import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CommentsService {
constructor(private http: HttpClient) {}
create(comment: any): Observable<any> {
return this.http.post(``, comment);
}
getComments(): Observable<any[]> {
return this.http.get<any[]>(``);
}
}
Сервис принимает в конструктор, HttpClient
и имеет два метода create
и getComments
которые отправлют POST и GET запросы. Запросы имееют пустое поле url
так как по сути являются "моковыми" и наше тестирование не выходит за рамки классов.
Создание spec файла
С компонентом и сервисом разобрались теперь создадим файл comments.component.spec.ts
в папке с компонентом и добавим в него следующее содержимое:
import { CommentsComponent } from './comments.component';
import { CommentsService } from '../shared/comments.service';
describe('CommentsComponent', () => {
let component: CommentsComponent;
let service: CommentsService;
});
Тут мы экпортировали наш тестируемый компонент CommentsComponent
и сервис CommentsService
, необходимый для работы нашего компонента. Также добавили обертку describe
для нашего тестового набора и также объявили переменные component
и service
для инициализации компонента и сервиса в тестах.
Запустим Karma командой ng test
. Откроется окно браузера с открытым тест раннером на порте 9876.
Отчет пустой, так как мы еще не добавили ни одного теста. Приступим к их написанию.
Добавление тестов
Открываем наш файл comments.component.spec.ts
и добавляем конструкцию beforeEach
в которой пропишем инициализацию нашего сервиса и компонента, при каждом запуске теста, что создаст изолированость для наших тестов. Также сервису необходимо передавать HttpClient
так как он принимает его в качестве параметра. Так как нам не нужно делать физические http-запросы (мы тестируем только код, а не api), мы сделаем "шпиона" для HttpClient
, с помощью метода createSpyObj
в Jasmine, и передадим его сервису.
import { CommentsComponent } from './comments.component';
import { CommentsService } from '../shared/comments.service';
describe('CommentsComponent', () => {
let component: CommentsComponent;
let service: CommentsService;
beforeEach(() => {
const spyHttp = jasmine.createSpyObj('HttpClient', { post: of({}), get: of({}) })
service = new CommentsService(spyHttp);
component = new CommentsComponent(service);
});
});
Далее через конструкцию it
добавим тест на то что наш в нашем компоненте вызывается метод getComments
в момент его инициализации (ngOnInit):
import { CommentsService } from '../shared/comments.service';
import { of, EMPTY } from 'rxjs'
import { CommentsComponent } from './comments.component';
describe('CommentsComponent', () => {
let component: CommentsComponent;
let service: CommentsService;
beforeEach(() => {
const spyHttp = jasmine.createSpyObj('HttpClient', { post: of({}), get: of({}) })
service = new CommentsService(spyHttp);
component = new CommentsComponent(service);
});
it('should call getComments when ngOnInit', () => {
const spy = spyOn(service, 'getComments').and.callFake(() => {
return EMPTY;
});
component.ngOnInit();
expect(spy).toHaveBeenCalled();
});
});
Тут мы "замокали" метод getComments
(т.е. следим за вызовом этого метода), с помощью метода spyOn
и положили все в переменную spy
. Если мы будет вызывать реальный метод getComments
то мы получим ошибку так как url у нас пустой. Собственно это нам и не нужно так как мы не тестируем api в данном случае.
Установив "слежку" за методом, мы можем им управлять. Для этого мы вызываем метод callFake
в который передаем колбек. Колбек должен нам вернуть какой либо observable
, согласно нашему методу getComments
. В данном случае нам не важно что он будет возвращать, поэтому в return
ставим константу EMPTY
которую импортируем из rxjs
.
Затем мы обращемся к компоненту и вызываем у него метод ngOnInit
. Далее ставим ожидание того что наш spy
(там наш замоканный метод getComments
) был вызван, при вызове метода ngOnInit
в компоненте, проверяя это с помощью метода toHaveBeenCalled
.
Проверям окно тест раннера и наш тест должен появится в отчете с успешным статусом.
Теперь добавим второй тест, который проверит что переменной comments
, в компоненте, присвоились какие либо данные в момент инициализации компонента. Для этого добавим следующий код:
describe('CommentsComponent', () => {
let component: CommentsComponent;
let service: CommentsService;
beforeEach(() => {
const spyHttp = jasmine.createSpyObj('HttpClient', { post: of({}), get: of({}) })
service = new CommentsService(spyHttp);
component = new CommentsComponent(service);
});
it('should call getComments when ngOnInit', () => {
const spy = spyOn(service, 'getComments').and.callFake(() => {
return EMPTY;
});
component.ngOnInit();
expect(spy).toHaveBeenCalled();
});
it('should update comment length after ngOnInit', () => {
const testComments = [1, 2, 3, 4]
spyOn(service, 'getComments').and.returnValue(of(testComments))
component.ngOnInit();
expect(component.comments.length).toBe(testComments.length);
});
});
Во втором тесте, мы создали переменную testComments
с массивом значений. Так же используем метод spyOn
в который передаем getComments
и добавляем метод returnValue
так как теперь мы будем возвращать уже не пустой observable
, а с массивом comments
. Для этого добавим метод of
из rxjs
.
Затем аналогично обращемся к компоненту и вызываем у него метод ngOnInit
. Далее добавляем ожидание того что наша переменная comments
в компоненте получила значения из testComments
при инициализации компонента. Следовательно наш метод getComments
отработал правильно.
Проверяем тест раннер и видим что второй тест успешно пройден.
Ну и добавим теперь тест добавления комментария.
import { CommentsService } from '../shared/comments.service';
import { of, EMPTY, throwError } from 'rxjs'
import { CommentsComponent } from './comments.component';
describe('CommentsComponent', () => {
let component: CommentsComponent;
let service: CommentsService;
beforeEach(() => {
const spyHttp = jasmine.createSpyObj('HttpClient', { post: of({}), get: of({}) })
service = new CommentsService(spyHttp);
component = new CommentsComponent(service);
});
it('should call getComments when ngOnInit', () => {
const spy = spyOn(service, 'getComments').and.callFake(() => {
return EMPTY;
});
component.ngOnInit();
expect(spy).toHaveBeenCalled();
});
it('should update comment length after ngOnInit', () => {
const testComments = [1, 2, 3, 4]
spyOn(service, 'getComments').and.returnValue(of(testComments))
component.ngOnInit();
expect(component.comments.length).toBe(testComments.length);
});
it('should add new comment', () => {
const testComment = {text: 'test'}
const spy = spyOn(service, 'create').and.returnValue(of(testComment))
component.add(testComment.text)
expect(spy).toHaveBeenCalled()
expect(component.comments.includes(testComment)).toBeTruthy()
});
});
Тут мы создали переменную testComment
которая содержит в себе объект нашего тестового комментария. Далее мы теперь аналогично "замокали" метод create
, с помощью метода spyOn
и добавили метод returnValue
так как у нас здесь будет возвращаться не пустой observable
а наш тестовый объект testComment
, и также использовали для этого метод of
из rxjs
. Затем положили все это в переменную spy.
Далее вызывается метод add
нашего компонента, к который передается значение ключа text нашего тестового объекта с комментарием (testObject). После проверяем ожидание того что spy
(там наш замоканный метод create
из сервиса) был вызван при вызове метода add
в компоненте. Затем проверяем что переменная с массивом comments
, в компоненте, получила наш тестовый коммент (методы includes
и toBeTruthy
).
Снова проверяем тест раннер и видим что тест прошел как и все остальные.
Заключение
В этой статье мы рассмотрели тестирование взаимодействия компонента с сервисом. Получилось слегка запутано и без базовового понимания rxjs, будет сложновато понять происходящее.