Тестирование сервисов


Начнем с изучения тестирования сервисов Angular, поскольку именно сервисы проще всего покрываются тестами.

Ключевую роль в тестировании Angular приложений играет утилита TestBed из библиотеки @angular/core/testing. Она позволяет эмулировать модуль Testing Module, подобный модулю, создаваемого с декоратором @NgModule(). Тестовый модуль необходим для определения модулей, сервисов, компонентов и т. д., от которых зависим тест.

В TestBed имеется метод configureTestingModule(), которая принимает объект конфигурации аналогичный тому, что передается @NgModule().


beforeEach(() => {
TestBed.configureTestingModule({
providers: [AppService],
})
})
В коде выше определенный в providers тестового модуля сервис AppService становится доступным для использования каждому из выполняемых тестов. Получение экземпляра сервиса осуществляется методом get() утилиты TestBed.

get() может предоставить только те сервисы, которые указаны в свойстве providers модуля Testing Module.


describe('AppService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AppService],
})

appService = TestBed.get(AppService)
})

it('getData() should multiply passed number by 2', () => {
spyOn(appService, 'getData').and.callThrough()

let a = appService.getData(2)
let b = appService.getData(3)

expect(a).toBe(4, 'should be 4')
expect(b).toBe(6, 'should be 6')

expect(appService.getData).toHaveBeenCalled()
expect(appService.getData.calls.count()).toBe(2)
expect(appService.getData.calls.mostRecent()).toBe(6)
})
})
Разберем пример. Здесь описан один тест, который проверяет корректность работы метода getData() сервиса AppService. Метод getData() принимает число и возвращает его удвоенное значение.

Для сбора информации о вызовах метода getData() используется spyOn().

Обычно сервис не ограничивается только собственными методами. В сложном приложении сервисы взаимодействуют между собой, используя функционал друг друга.

В таких случаях общепринято использовать объекты Spy, чтобы не создавать ради одного или нескольких методов полноценный экземпляр еще одного сервиса.


describe('AppService', () => {
beforeEach(() => {
const appServiceSpy = jasmine.createSpyObj(
'AppService',
{
getData: [1, 2, 3],
}
)

TestBed.configureTestingModule({
providers: [
{ provide: AppService, useValue: appServiceSpy },
],
})

appService = TestBed.get(AppService)
})

it('emulate getData usage', () => {
const data = [1, 2, 3]

appService.getData.and.returnValue(data)

expect(appService.getData().length).toBe(
data.length,
'length should be 3'
)
})
})
В примере createSpyObj() эмулирует сервис AppService с его единственным методом getData().

Если все тесты работают с одним набором данных, которые должен возвращать метод getData(), то в beforeEach() задать эти данные можно так:


const appServiceSpy = jasmine.createSpyObj('AppService', {
getData: [1, 2, 3],
})
Инструменты также предусматривают возможность тестирования сервисов Angular, которые обращаются за данными к удаленному серверу. Ключевую роль здесь играют модуль HttpTestingModule и контроллер HttpTestingController.

Тестирование HTTP-сервисов не подразумевает обращение к удаленному API. Вместо этого все исходящие запросы перенаправляются в контроллер HttpTestingController.

app.service.ts


import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'

@Injectable({ providedIn: 'root' })
export class AppService {
constructor(private http: HttpClient) {}

getData() {
return this.http.get(`/api/data`)
}
}
app.service.spec.ts


import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { TestBed } from '@angular/core/testing'

describe('AppService - testing HTTP request method getData()', () => {
let httpTestingController: HttpTestingController

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [AppService],
})

appService = TestBed.get(AppService)
httpTestingController = TestBed.get(
HttpTestingController
)
})

it('can test HttpClient.get', () => {
const data = [1, 2, 3]

appService
.getData()
.subscribe((response) => expect(response).toBe(data))

const req = httpTestingController.expectOne('/api/data')

expect(req.request.method).toBe('GET')

req.flush(data)
})

afterEach(() => httpTestingController.verify())
})
Как видно из примера, доступ к объекту запроса осуществляется с использованием метода expectOne() экземпляра класса HttpTestingController, идентифицирующего запрос в зависимости от переданного ему условия. Метод принимает параметром URL, на который осуществляется запрос, либо сам объект запроса. Например, можно отловить запрос с наличием определенного HTTP-заголовка или с определенным его значением.

Условию должен удовлетворять только один запрос. Если таких запросов будет несколько или они будут отсутствовать вовсе, будет сгенерировано исключение. Для работы с группой запросов необходимо использовать метод match(), который возвращает массив HTTP-запросов, попадающих под заданный критерий.


const req = httpTestingController.match('/api/data')
В приведенном коде переменная req будет содержать массив всех запросов, сделанных на URL /api/data.

Возвращаемые в ответ на запрос данные передаются аргументом методу flush().

В конце каждого такого теста у экземпляра класса HttpTestingController нужно вызывать метод verify(), который подтверждает, что все запросы в рамках текущего теста были выполнены. Код идеально подходит для размещения в функции afterEach().

Для эмуляции ответа сервера с кодом ошибки, вторым аргументом методу flush() передается объект, где указывается статус и текст ошибки.


it('can test HttpClient.get', () => {
const message = 'Session expired'

appService.getData().subscribe(
(response) => fail('should fail with the 401 error'),
(err: HttpErrorResponse) => {
expect(err.status).toBe(401, 'status')
expect(err.error).toBe(message, 'message')
}
)

const req = httpTestingController.expectOne('/api/data')

expect(req.request.method).toBe('GET')

req.flush(message, {
status: 401,
statusText: 'Unauthorized',
})
})
Для ошибки сетевого уровня можно использовать метода error() объекта запроса. Передаваемый параметр - объект типа ErrorEvent.


const error = new ErrorEvent('Network error', {
message: 'Something wrong with network',
})

req.error(error)
В приведенных примерах fail() используется для принудительного завершения выполнения теста с ошибкой в тех местах, где Angular не сможет самостоятельно определить, правильно ли был выполнен сценарий.