Тестируем Angular приложение. Часть 2. Тестирование сервиса

от автора

В прошлой статье, я описывал как тестировать компонент. Теперь же коснемся вопроса тестирования сервиса.

Вступление

В данном примере будем использовать так же, инструменты идущие из «коробки», такие как 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *