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

Вступление

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

Комментарии (0)