Тестирование Angular. Основы. Тестирование сервисов

Стандартные инструменты для тестирования

  • Jasmine – фреймворк для юнит-тестирования с простым синтаксисом (аналог популярного Jest).
  • Karme – тест-ранер, инструмент, разработанный командой Ангуляра для запуска и прогона тестов. Берет все тесты, загружает и прогоняет в браузере.
  • Protractor – относится к e2e тестам, поэтому здесь не рассматривается.

Структура тестирования в Ангуляр

Методы проверки ожиданий (“матчеры”):

.toBe() ===
.toEqueal() Сравнивает объекты со сложной структурой
.toBeDefined(), toBeUndefined() Проверяет значение не defined
.toBeTruthy() проверяет логическое значение
.toBeFalsy() проверяет логическое значение

Для проверки на ложноположительный результат можно добавлять .not. Например, .not.toBeTruthy()

Примеры тестов сервиса:

1. Максимально простой пример

simple.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class SimpleService {

  sum(a: number, b?: number) {
    if (!b) {
      return undefined;
    } else {
      return a + b;
    }
  }
}

simple.service.spec.ts

import { SimpleService } from './simple.service';

describe('Service: Simple', () => {
  let service: SimpleService;

  beforeEach(() => {
    service = new SimpleService; // запускатся перед каждым тестом, чтобы не дублировать инициализацию
  });

  it('должен создавать экземпляр класса', () => {
    expect(service).toBeTruthy();
  });

  it("должен сумировать два числа", () => {
    const sum = service.sum(1, 2);
    expect(sum).toBe(3);
  })

  it("c одним аргументом undefined", () => {
    const sum = service.sum(1);
    expect(sum).toBeUndefined;
  })
});
Пример простого тестирования Angular
Максимально простой пример тестирования сервиса

2. Сервис с зависимостями без утилит Angular

Но на практике сервисы имеют зависимости (подключенные другие сервисы в конструкторе). И такой простой пример уже не отработает.

...
export class SimpleService {
  constructor(private checkValueService: CheckValueService) {}
...
}

Будет результат, что при инициализации наш SimpleService упал (так как инициализировался без своей зависимости), тестов в проекте покажет 0 и 100% завершены.

Самый простой способ быстро исправить это, просто передать в параметрах зависимость при инициализации (в параметры класса передать другой сервис):

....
describe('Service: Simple', () => {
  let service: SimpleService;

  beforeEach(() => {
    service = new SimpleService(new CheckValueService()); //передали параметром зависимость
  });
....

Но это плохая практика. Во-первых, так как у CheckValueService могут появиться свои зависимости, то их тоже надо будет так передавать. Во-вторых, мы нарушим другое фундаментальное правило написания тестов – тесты должны быть изолированы, а любые внешние зависимости должны мокаться.

Для примера создадим новый объект fakeValueService, содержащий такой же метод check, как в оригинальном сервисе. Это и будет самый простой, неудобный и негибкий, но рабочий, пример, как можно замокать зависимость:

...
describe('Service: Simple', () => {
  let service: SimpleService;
  const fakeValueService = {check: () => true}; //создали моки

  beforeEach(() => {
    service = new SimpleService(fakeValueService as CheckValueService); //передали вместо настоящего сервиса
  });
...

2. Использование утилиты Angular TestBed

При создании сущностей ангуляровских мы используем dependency injection, то было бы неплохо применить его в тестах. Для этого перепишем файл с тестами с использованием утилиты TestBed.

TestBed – утилита ангуляр, которая создает тестовое окружение для тестов. Ее метод configureTestingModule По сути создает мок модуля, точно такой же, как ангуляр-модуль.

describe('Service: Simple', () => {
  let service: SimpleService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [SimpleService]
    });

    service = TestBed.inject(SimpleService); // в старых версиях TestBed.get
  });

...

В даном простом примере тесты отработают и так. Потому что Angular testBed при отсутствии указанных зависимостей пытается использовать настоящие зависимости (здесь это CheckValueService). Но так оставлять нельзя (да и при более сложном сервисе это упадет), поэтому надо еще указать и зависимости вот так:

describe('Service: Simple', () => {
  let service: SimpleService;
  const fakeValueService = {check: () => true};

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        SimpleService,
        {provide: CheckValueService, useValue: fakeValueService} // указываем зависимости, хотя без них тесты отрабатывают у меня...
      ]
    });

    service = TestBed.inject(SimpleService); // в старых версиях TestBed.get
  });
...

Но не всегда использование утилиты TestBed оправдано! Она замедляет выполнение тестов. Поэтому в случаях, когда тестируется не класс, а какой-то ряд функций, то она абсолютно избыточна. В остальных случаях TestBed удобен и берет на себя рутину.

Как отключить тесты

Временно отключаем падающие тесты

Если иногда какой-то тест падает, а времени, чтобы разобраться и подчинить его нет перед горящим релизом, то можно временно отключать отдельные тесты или целые группы. Для этого к названию функции добавляется “x” и получается xit или xdescribe соответственно.

xdescribe('Service: CheckValue', () => { //отключили группу
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [CheckValueService]
    });
  });

  xit('should ...', inject([CheckValueService], (service: CheckValueService) => { //отключили один тест
    expect(service).toBeTruthy();
  }));
});

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

Временно отключаем все остальные тесты, которые пока не нужны

Если в процессе разработки нас интересует сейчас только какой-то один тест или группа, то к названию функции для вызова теста или группы тестов дописываем букву “f” и получаем fit или fdescribe соответственно.

В результатах все остальные тесты будут помечены серым.

Отчет по покрытию проекта тестами (TEST COVERAGE REPORT)

Чтобы отобразить такой отчет нужно добавить к функции запуска тестов флаг –code-coverage:

ng test --code-coverage

Или же можно один раз в файле angular.json в секции  test добавить опцию “codeCoverage” со значением true:

        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.spec.json",
            "karmaConfig": "karma.conf.js",
            "inlineStyleLanguage": "scss",
            "codeCoverage": true,
...

В терминале мы увидим краткий отчет!

Но основной отчет можно посмотреть в папке проекта coverage/project-name/index-html:

Отчет о покрытии тестов в Ангуляре

  • Statements – количество исполняемых инструкций, покрытых юнит-тестами.
  • Branchec –  количество исполняемых ветвлений (внутри метода), покрытых юнит-тестами.
  • Functions – количество исполняемых функций, покрытых юнит-тестами.
  • Lines – количество исполняемых строк, покрытых юнит-тестами.

Можно переходить по ссылкам и смотреть подробнее вплоть до кода, где не хватает тестов.

Настройка минимального процента покрытия тестами

Это делается в файле karma.conf.js в разделе coverageReporter в объекте check (ранее thresholds):

 coverageReporter: {
      dir: require('path').join(__dirname, './coverage/karma-test'),
      subdir: '.',
      reporters: [
        { type: 'html' },
        { type: 'text-summary' }
      ],
      check: { // в старых версиях "thresholds"
        emitWarning: false, // ворнинги о непокрытии тестами (будут красными)
        global: {
          statements: 100, // минимальные значения покрытия в процентах
          lines: 100,
          branches: 100,
          functions: 100
        }
      }
...

Недостаточная покрытость тестами

Для того, чтобы Карма упала даже с работающими тестами, нужно запускать флаг –watch в значении false значение:

ng test --no-watch --code-coverage

 

ИТОГ

Итог по тестирования Ангуляр

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

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