
В прошлой статье, я описывал как тестировать компонент. Теперь же коснемся вопроса тестирования сервиса.
Вступление
В данном примере будем использовать так же, инструменты идущие из «коробки», такие как 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, будет сложновато понять происходящее.
ссылка на оригинал статьи https://habr.com/ru/articles/593501/
Добавить комментарий