Давайте обсудим модульное тестирование в Angular, это будет просто введение в модульное тестирование Angular и преимущества использования Jest в качестве средства запуска тестов и пакета для вашего выполнения.
Какие?
Jest – это интегрированное решение для тестирования, написанное Facebook и особенно известное в мире React.
Ключевые особенности Jest:
- Простая настройка
- Мгновенная обратная связь
- Мощное издевательство
- Работает с машинописным текстом
- Моментальное тестирование
Простая настройка означает, что вам не нужно практически ничего настраивать, чтобы начать работу.
Мгновенная обратная связь, потому что он будет запускать только тестовые файлы, связанные с изменениями.
Мощный макет с помощью простых в использовании функций макета
Почему?
Первая причина, по которой вы хотите начать использовать шутку, — это скорость.
Модульный тест должен работать быстро
Сравнение нового приложения, созданного с @angular/cli
karma-chrome: 14,911 с.
karma-phantomjs: 13,119 с.
шутка: 4,970 с.
Это разница между двумя запусками составляет 8,606 секунды, karma-chrome занимает более чем вдвое больше времени, чтобы выполнить всего 1 набор и 3 теста.
Я включаю PhantomJS в эти сравнения, даже если он больше не поддерживается, главным образом потому, что это, вероятно, самый быстрый вариант для запуска тестов в среде CI (Дженкинс, Трэвис).
Jest не требует реального браузера (безголового или нет) для запуска теста (есть некоторые недостатки). Если вы ищете замену PhantomJS в средах непрерывной интеграции, вы можете быстро переключиться на Jest без необходимости какой-либо настройки вашего CI.
Он основан на Jasmine, который, вероятно, является фреймворком по умолчанию для приложений Angular и включен в CLI.
Как?
Первый шаг — установить jest в ваш новый проект:
$ yarn add -D @types/jest jest jest-preset-angular
Установка типов, необходимых для компилятора машинописного текста, самого фреймворка и в файле jest-preset-angular
, который содержит конфигурацию для проекта Angular.
Следующий шаг, изменить package.json
:
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"lint": "ng lint",
"e2e": "ng e2e",
"test": "jest",
"test:watch": "jest --watch"
},
"jest": {
"preset": "jest-preset-angular",
"setupTestFrameworkScriptFile": "<rootDir>/src/jest.ts"
}
Я меняю npm-скрипты, которые будут запускать jest framework и добавляю единственную конфигурацию (почти нулевой конфиг), которая нам нужна для проекта.
Я требую, чтобы "setupTestFrameworkScriptFile": "<rootDir>/src/jest.ts"
нам нужно было создать этот файл, который Jest будет использовать для запуска внутри папки src проекта.
import 'jest-preset-angular';
import './jest-global-mocks';
Последний шаг, так как Jest не запускает настоящий браузер, а основан на jsdom, заключается в создании макетов для конкретного API браузера, такого как localStorage
и т. д.:
const mock = () => {
let storage = {};
return {
getItem: key => (key in storage ? storage[key] : null),
setItem: (key, value) => (storage[key] = value || ''),
removeItem: key => delete storage[key],
clear: () => (storage = {})
};
};
Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
Object.defineProperty(window, 'getComputedStyle', { value: () => ['-webkit-appearance']
});
И последнее, но не менее важное: мы должны удалить @types/jasmine
из node_modules и убедиться, что вы включили jest в качестве новых типов в tsconfig.spec.json.
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}
Jest основан на Jasmine, поэтому нет необходимости изменять тест, уже присутствующий в шаблонном приложении.
Единственная заметная разница между Jest и Jasmine — это шпионы. Шпион в Jest по умолчанию будет похож на callThrough
, вам нужно будет издеваться над ним, если вы хотите подражать шпионам Jasmine.
Теперь мы можем приступить к тестированию нашего приложения и использовать функции имитации, которые предоставляет Jest.
Услуги
Сервисы, вероятно, являются одним из самых простых элементов для тестирования. В конце концов, это всего лишь классы ES6, и вы можете (и должны) тестировать их напрямую, без использования TestBed. Причина, по которой я говорю, что это множественность.
Этот код основан на новом HttpClient
, выпущенном с Angular 4.3, который также упрощает работу при использовании TestBed.
У нас есть следующий API:
{
"DUB": {
"name": "Dublin"
...
},
"MAD": {
"name": "Madrid"
...
},
...
}
И наш сервис должен получить эти аэропорты и упорядочить их по коду, а также получить один аэропорт. Я буду использовать ramda, чтобы уменьшить шаблон нашего кода и оживить его функциональным программированием.
Итак, первый будет называться fetchAll$
public fetchAll$(): Observable<any> {
return this.http.get('https://foo.bar.com/airports')
.map(toPairs)
.map(sortBy(compose(
toLower,
head
)));
}
Сначала мы преобразуем API в пары [ключ, значение] с помощью функции toPairs
, а после сортируем все аэропорты по их кодам.
Вместо этого второй метод нашего сервиса должен получить один аэропорт:
public fetchByIATA$(iata: string): Observable<any|undefined> {
return this.http.get('https://foo.bar.com/airports')
.map(prop(iata));
}
fetchByIATA$
просто верните значение определенного ключа, в данном случае код IATA, или неопределенное, используя функцию prop
из ramda.
Вот и вся служба:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map'
import {
toPairs,
compose,
head,
toLower,
prop,
sortBy
} from 'ramda';
@Injectable()
export class SampleService {
constructor(private http: HttpClient) {}
public fetchAll$(): Observable<any> {
return this.http.get('https://foo.bar.com/airports')
.map(toPairs)
.map(sortBy(compose(
toLower,
head
)));
}
public fetchByIATA$(iata: string): Observable<any|undefined> {
return this.http.get('https://foo.bar.com/airports')
.map(prop(iata));
}
}
Здесь нам нужно имитировать ответ http, потому что мы не хотим загружать сервер для каждого теста, и помните, что мы проводим модульные тесты, а не сквозные или интеграционные тесты.
С новым HttpClient
нет необходимости настраивать mockBackend, и это такое облегчение.
Прежде всего, мы должны настроить TestBed для загрузки нашего сервиса с помощью HttpClientTestingModule, это даст нам возможность перехватывать и имитировать наши внутренние вызовы.
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ],
providers: [
SampleService
]
});
});
После настройки TestBed мы теперь можем получить наш сервис для тестирования и фиктивный http-контроллер из инжектора:
beforeEach(
inject([SampleService, HttpTestingController], (_service, _httpMock) => {
service = _service;
httpMock = _httpMock;
}));
Теперь, когда у нас есть все настройки, мы можем приступить к
одиночным тестам.
it('fetchAll$: should return a sorted list', () => {
const mockAirports = {
DUB: { name: 'Dublin' },
WRO: { name: 'Wroclaw' },
MAD: { name: 'Madrid' }
};
service.fetchAll$().subscribe(airports => {
expect(airports.length).toBe(3);
expect(airports[2][0]).toBe('WRO');
});
const req = httpMock.expectOne('https://foo.bar.com/airports');
req.flush(mockAirports);
httpMock.verify();
});
Новый HttpClient
на самом деле напоминает мне AngularJS v1.x способ тестирования HTTP-вызовов. Мы определяем, что мы ожидаем от вызова функции, а затем через объект httpMock
мы указываем, какие вызовы мы ожидаем и что возвращать flush
. В конце мы вызываем функцию verify()
, чтобы убедиться, что нет ожидающих соединений.
Вот ссылка на полный исходный код
При использовании Jest предыдущий набор займет это время:
Другой вариант, который мы можем изучить, — это имитировать непосредственно HttpClient
, в этом примере нас интересует только функция get
, которая должна возвращать наблюдаемую, содержащую наши данные.
Мы вообще не будем использовать TestBed, а просто воспользуемся фиктивной функцией, которую предоставляет Jest.
SampleService
требует HttpClient
в конструкторе:
const http = {
get: jest.fn()
};
beforeEach(() => {
service = new SampleService(http);
});
Мы передадим нашу заглушку в SampleService, и typescript сообщит об отсутствии свойств для HttpClient. Чтобы преодолеть это, мы можем:
Всегда используйте as any
:
beforeEach(() => {
service = new SampleService(http as any);
});
Если вы не хотите повторять как любое ключевое слово или можете создать небольшую функцию, которая сделает это за вас, а затем
при импорте:
const provide = (mock: any): any => mock;
...
beforeEach(() => {
service = new SampleService(provide(http));
});
На этом этапе мы можем указать тест следующим образом:
it('fetchAll$: should return a sorted list', () => {
http.get.mockImplmentationOnce(() => Observable.of(mockAirports));
service.fetchAll$().subscribe((airports) => {
expect(http.get).toBeCalledWith('https://foo.bar.com/airports');
expect(airports.length).toBe(3);
expect(airports[2][0]).toBe('WRO');
});
});
Мы вызываем mockImplementationOnce
, как следует из названия, чтобы смоделировать его только один раз и вернуть Observable нашего mockAirports, мы повторяем то же самое утверждение, что и раньше.
Вот ссылка на полный исходный код
А это время казни:
Подытожим: пакет, выполняющий два теста с TestBed, занимает в общей сложности 99 мс, а без TestBed — всего 12 мс.
Обратите внимание, что я уже использовал расширенные мок-функции во второй тестовой шутке. Я запрашиваю фиктивную функцию напрямую через jest.fn()
, если вы хотите узнать больше об этих фиктивных функциях, посмотрите здесь.
Окончательное сравнение
Теперь, когда у нас есть эти два дополнительных модульных теста, давайте попробуем еще раз запустить набор двух модульных тестов, один с Karma + Chrome, другой с Jest, и посмотрим на результаты.
Я создал следующий скрипт для отслеживания времени на моей локальной машине:
start=$SECONDS
yarn test -- --single-run
end=$SECONDS
echo "duration: $((SECONDS-start)) seconds elapsed.."
карма + хром
Я добавил karma-verbose-reporter, чтобы получить больше информации о тестах, и общий результат составил 22 секунды.
Вместо этого для Jest скрипт для отслеживания времени выглядит следующим образом:
start=$SECONDS
yarn test -- --verbose
end=$SECONDS
echo "duration: $((SECONDS-start)) seconds elapsed.."
--verbose
будет отслеживать выполнение каждого теста внутри пакета.
шутка
Jest по-прежнему лидирует с выполнением всего 5 секунд.
Бонус
Я упоминал перед мгновенным отзывом, что Jest автоматически запускает тесты, связанные с файлом, который вы изменили. Это особенно хорошо во время просмотра.
Если мы зафиксируем изменения в нашем репозитории и попытаемся изменить только app.component.ts
во время работы yarn test:watch
, вы заметите, что работает только app.component.spec.ts
:
Вывод
Я действительно считаю, что фреймворки должны поощрять тестирование, и решающим моментом является их скорость. Jest предоставляет все это, и мы уже перешли в Ryanair с karma + chrome на Jest.
PS: Если вам небезразличны модульные тесты и Angular, которых мы нанимаем в Ryanair, присоединяйтесь к нам.
Первоначально опубликовано на izifortune.com 26 июля 2017 г.