Перейти к содержанию

Unit-тестирование. Сервисы

Начнем с изучения тестирования сервисов 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 не сможет самостоятельно определить, правильно ли был выполнен сценарий.