Тестирование 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; }) });
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