Тестирование
Методы и практики тестирования приложения Angular.
Это руководство предоставляет советы и методы для тестирования приложений Angular. Хотя эта страница включает некоторые общие принципы и методы тестирования, основной упор делается на тестирование приложений, написанных с использованием Angular.
Содержание
- Живые примеры
- Введение в тестирование Angular
- Первый тест Karma
- Тестирование компонента
- Тестирование компонента с внешним шаблоном
- Тестирование компонента с зависимостью от сервиса
- Тестирование компонента с асинхронным сервисом
- Тестирование компонента с входными и выходными данными
- Тестирование компонента внутри компонента-хоста теста
- Тестирование маршрутизированного компонента
- Использование объекта page для упрощения настройки
- Настройка с импортами модулей
- Импорт модуля функции
- Переопределение поставщиков компонента
- Тестирование компонента RouterOutlet
- "Поверхностные тесты компонентов" с NO_ERRORS_SCHEMA
- Тестирование атрибутивной директивы
- Изолированные модульные тесты
- API утилит тестирования Angular
- Файлы настройки тестовой среды
- FAQ: Часто задаваемые вопросы
Это большая программа. К счастью, вы можете учиться понемногу и применять каждый урок.
Живые примеры
В этом руководстве представлены тесты образцового приложения, которое очень похоже на туториал «Обзор героев». Образцовое приложение и все тесты в этом руководстве доступны в виде живых примеров для просмотра, экспериментов и скачивания:
- Спецификация для проверки тестовой среды.
- Первый компонент спецификации со встроенным шаблоном.
- Спецификация компонента с внешним шаблоном.
- Спецификация компонента AppComponent seed-проекта QuickStart.
- Образцовое приложение для тестирования.
- Все спецификации, которые тестируют образцовое приложение.
- Разнообразные дополнительные спецификации.
Введение в тестирование Angular
Эта страница проведет вас через написание тестов для изучения и подтверждения поведения приложения. Тестирование выполняет следующие действия:
-
Защищает от изменений, нарушающих существующий код («регрессии»).
-
Поясняет, что делает код как при целевом использовании, так и при отклоняющихся условиях.
-
Выявляет ошибки в дизайне и реализации. Тесты освещают код с разных углов. Когда часть приложения кажется трудно тестируемой, часто корневой причиной является дефект дизайна, который нужно исправить сейчас, а не позже, когда его исправление станет дорогостоящим.
Инструменты и технологии
Вы можете писать и запускать тесты Angular с помощью различных инструментов и технологий. Это руководство описывает конкретные варианты, которые хорошо зарекомендовали себя.
| Технология | Назначение |
|---|---|
| Jasmine |
Фреймворк для тестирования Jasmine предоставляет все необходимое для написания базовых тестов. Он поставляется с HTML-запускателем тестов, который выполняет тесты в браузере. |
| Утилиты тестирования Angular |
Утилиты тестирования Angular создают тестовую среду для кода приложения Angular, подлежащего тестированию. Используйте их для управления и контроля частей приложения, когда они взаимодействуют *внутри* среды Angular. |
| Karma |
Запускатель тестов Karma идеально подходит для написания и запуска модульных тестов во время разработки приложения. Он может быть неотъемлемой частью процессов разработки проекта и непрерывной интеграции. Это руководство описывает, как настроить и запустить тесты с помощью Karma. |
| Protractor |
Используйте Protractor для написания и запуска тестов *по всему приложению* (e2e). Тесты по всему приложению исследуют приложение *так, как его видят пользователи*. В тестировании по всему приложению один процесс запускает реальное приложение, а второй процесс запускает тесты Protractor, которые имитируют поведение пользователя и проверяют, отвечает ли приложение в браузере ожидаемым образом. |
Настройка
Есть два быстрых способа начать работу с модульным тестированием.
-
Создайте новый проект, следуя инструкциям в разделе Настройка.
-
Создайте новый проект с помощью Angular CLI.
Оба подхода устанавливают npm пакеты, файлы и скрипты, предварительно настроенные для приложений, созданных в соответствующих моделях. Их артефакты и процедуры различаются незначительно, но их основы одинаковы, и в коде тестов нет различий.
В этом руководстве приложение и его тесты основаны на инструкциях по настройке. Для обсуждения файлов настройки модульного тестирования см. ниже.
Изолированные модульные тесты против утилит тестирования Angular
Изолированные модульные тесты исследуют экземпляр класса в отдельности, без какой-либо зависимости от Angular или каких-либо инжектированных значений. Тестировщик создаёт тестовый экземпляр класса с new, обеспечивая тестовые заглушки для параметров конструктора по мере необходимости, а затем исследует поверхность API тестового экземпляра.
Вы должны писать изолированные модульные тесты для трубок и сервисов.
Вы также можете тестировать компоненты в изоляции. Однако изолированные модульные тесты не показывают, как компоненты взаимодействуют с Angular. В частности, они не могут показать, как класс компонента взаимодействует с собственным шаблоном или с другими компонентами.
Для таких тестов необходимы **утилиты тестирования Angular**. Утилиты тестирования Angular включают класс TestBed, а также несколько вспомогательных функций из @angular/core/testing. Они являются основным предметом этого руководства, и вы узнаете о них при написании первого теста компонента ниже. Полное описание утилит тестирования Angular приведено позже в этом руководстве.
Но сначала вы должны написать фиктивный тест, чтобы проверить, что ваша тестовая среда настроена правильно и закрепить несколько основных навыков тестирования.
Первый тест karma
Начните с простого теста, чтобы убедиться, что настройка работает правильно.
Создайте новый файл, названный 1st.spec.ts в корневой папке приложения, src/app/
Тесты, написанные в Jasmine, называются спецификациями. Расширение имени файла должно быть
.spec.ts, соглашение, которому следуютkarma.conf.jsи другие инструменты.
Поместите файлы спецификаций где-нибудь в папке src/app/. karma.conf.js сообщает karma искать файлы спецификаций там по причинам, объяснённым ниже ниже.
Добавьте следующий код в src/app/1st.spec.ts.
src/app/1st.spec.ts
describe('1st tests', () => {
it('true is true', () => expect(true).toBe(true));
});
Запуск с karma
Скомпилируйте и запустите его в karma из командной строки, используя следующую команду:
npm test
Команда компилирует код приложения и тестов и запускает karma. Оба процесса отслеживают соответствующие файлы, записывают сообщения в консоль и повторно запускаются при обнаружении изменений.
Настройка документации определяет команду
testв разделеscriptsnpmpackage.json. Angular CLI имеет разные команды для выполнения той же задачи. Подстройте под это.
Через несколько мгновений karma откроет браузер и начнёт запись в консоль.
Спрячьте (не закрывайте!) браузер и сосредоточьтесь на выводе консоли, который должен выглядеть примерно так:
> npm test
...
[0] 1:37:03 PM - Compilation complete. Watching for file changes.
...
[1] Chrome 51.0.2704: Executed 0 of 0 SUCCESS
Chrome 51.0.2704: Executed 1 of 1 SUCCESS
SUCCESS (0.005 secs / 0.005 secs)
Как компилятор, так и karma продолжают работать. Вывод компилятора предваряется [0]; вывод karma – [1].
Измените ожидание с true на false.
Наблюдатель компилятора обнаруживает изменение и перекомпилирует.
[0] 1:49:21 PM - File change detected. Starting incremental compilation... [0] 1:49:25 PM - Compilation complete. Watching for file changes.
Наблюдатель karma обнаруживает изменение в выводе компиляции и повторно запускает тест.
[1] Chrome 51.0.2704 1st tests true is true FAILED [1] Expected false to equal true. [1] Chrome 51.0.2704: Executed 1 of 1 (1 FAILED) (0.005 secs / 0.005 secs)
Конечно, он завершается неудачей.
Восстановите ожидание с false до true. Оба процесса обнаруживают изменение, повторно запускаются, и karma сообщает об успешном завершении.
Лог консоли может быть довольно длинным. Следите за последней строкой. Если всё хорошо, она выглядит как
SUCCESS.
Отладка тестов
Отлаживайте спецификации в браузере так же, как отлаживаете приложение.
- Раскрыйте окно браузера karma (скрытое ранее).
- Нажмите кнопку DEBUG; она откроет новую вкладку браузера и повторно запустит тесты.
- Откройте «Инструменты разработчика» браузера (
Ctrl-Shift-Iв Windows;Command-Option-Iв OSX). - Выберите раздел «Источники».
- Откройте файл теста
1st.spec.ts(Control/Command-P, затем начните вводить имя файла). - Установите точку останова в тесте.
- Обновите браузер, и он остановится на точке останова.
Попробуйте пример в реальном времени
Вы также можете попробовать этот тест в plunker. Все тесты в этом руководстве доступны в виде примеров в реальном времени.
Тестирование компонента
Компонент Angular — это первое, что большинство разработчиков хотят протестировать. BannerComponent в src/app/banner-inline.component.ts — это самый простой компонент в этом приложении, и с него хорошо начать. Он отображает заголовок приложения в верхней части экрана внутри тега <h1>.
src/app/banner-inline.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
Эта версия BannerComponent имеет встроенный шаблон и привязку интерполяции. Компонент, вероятно, слишком прост, чтобы его стоило тестировать в реальной жизни, но он идеально подходит для первого знакомства с инструментами тестирования Angular.
Соответствующая спецификация src/app/banner-inline.component.spec.ts находится в той же папке, что и компонент, по причинам, объяснённым в ответе на вопрос FAQ "Почему спецификации находятся рядом с теми вещами, которые они тестируют?".
Начните с операторов импорта ES6 для доступа к символам, на которые ссылается спецификация.
src/app/banner-inline.component.spec.ts (импорты)
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-inline.component';
Вот describe и beforeEach перед тестами:
src/app/banner-inline.component.spec.ts (beforeEach)
describe('BannerComponent (inline template)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
});
TestBed
TestBed — это первый и самый важный инструмент тестирования Angular. Он создаёт модуль тестирования Angular — класс @NgModule — который вы настраиваете методом configureTestingModule для создания среды модуля для класса, который вы хотите протестировать. По сути, вы отделяете тестируемый компонент от его собственного модуля приложения и снова прикрепляете его к динамически созданному модулю тестирования Angular, настроенному специально для этой серии тестов.
Метод configureTestingModule принимает похожий на @NgModule метаданные объект. Объект метаданных может содержать большинство свойств обычного модуля Angular модуля.
Этот объект метаданных просто объявляет тестируемый компонент, BannerComponent. В метаданных отсутствуют imports, потому что (а) стандартная конфигурация тестируемого модуля уже содержит то, что BannerComponent нужно, и (б) BannerComponent не взаимодействует ни с какими другими компонентами.
Вызовите configureTestingModule внутри beforeEach таким образом, чтобы TestBed мог сбрасывать себя в начальное состояние перед запуском каждого теста.
Начальное состояние включает в себя стандартную конфигурацию модуля тестирования, состоящую из декларируемых (компонентов, директив и труб) и поставщиков (некоторые из них имитированы), которые нужны почти всем.
Упомянутые ниже шимы тестирования
BrowserModuleинициализируют конфигурацию модуля тестирования примерно так же, как@angular/platform-browser.
Эта базовая конфигурация — всего лишь основа для тестирования приложения. Позже вы вызовете TestBed.configureTestingModule с дополнительными метаданными, которые определят дополнительные импорты, объявления, поставщиков и схемы, соответствующие тестам вашего приложения. Дополнительные методы override могут уточнить аспекты конфигурации.
createComponent
После настройки TestBed, вы говорите ему создать экземпляр компонента-под-тестом. В этом примере TestBed.createComponent создаёт экземпляр BannerComponent и возвращает фиксатуру теста компонента.
Не перенастраивайте
TestBedпосле вызоваcreateComponent.
Метод createComponent закрывает текущий экземпляр TestBed для дальнейшей настройки. Вы не можете больше вызывать методы настройки TestBed, ни configureTestingModule, ни какие-либо другие методы override.... Если вы попытаетесь, TestBed выдаст ошибку.
ComponentFixture, DebugElement и query(By.css)
Метод createComponent возвращает ComponentFixture, указатель на тестовую среду, окружающую созданный компонент. Фиксатура предоставляет доступ к самому экземпляру компонента и к DebugElement, указателю на DOM-элемент компонента.
Значение свойства title интерполируется в DOM внутри тегов <h1>. Используйте метод DebugElement фиксатуры, чтобы получить элемент <h1> по селектору CSS.
Метод query принимает предикатную функцию и ищет в дереве DOM фиксатуры первый элемент, удовлетворяющий предикату. Результатом является разный DebugElement, связанный с соответствующим DOM-элементом.
Метод
queryAllвозвращает массив всехDebugElements, удовлетворяющих предикату.Предикат — это функция, которая возвращает булево значение. Предикат запроса получает
DebugElementи возвращаетtrueв случае, если элемент соответствует критериям выбора.
Класс By — это утилита тестирования Angular, которая генерирует полезные предикаты. Его статический метод By.css создаёт предикат стандартного селектора CSS, который фильтрует так же, как селектор jQuery.
Наконец, настройка присваивает DOM-элемент из свойства nativeElement DebugElement к el. Тесты проверяют, что el содержит ожидаемый текст заголовка.
Тесты
Jasmine выполняет функцию beforeEach перед каждым из этих тестов
src/app/banner-inline.component.spec.ts (тесты)
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
Эти тесты запрашивают у DebugElement родной HTML-элемент, чтобы удовлетворить свои ожидания.
detectChanges: Изменение обнаружения Angular в тесте
Каждый тест говорит Angular, когда выполнять обнаружение изменений, вызывая fixture.detectChanges(). Первый тест делает это сразу, вызывая привязку данных и распространение свойства title на DOM-элемент.
Второй тест изменяет свойство компонента title и только затем вызывает fixture.detectChanges(); новое значение появляется в DOM-элементе.
В процессе разработки обнаружение изменений происходит автоматически при создании компонента Angular или вводе пользователем символа или завершении асинхронной задачи (например, AJAX).
TestBed.createComponent не вызывает обнаружение изменений. Фиксатура не автоматически помещает значение свойства компонента title в элемент привязки данных, что демонстрируется в следующем тесте:
src/app/banner-inline.component.spec.ts (без detectChanges)
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
Это поведение (или его отсутствие) является намеренным. Оно даёт тестировщику возможность проверить или изменить состояние компонента до того, как Angular инициирует привязку данных или вызовет жизненный цикл.
Попробуйте пример в реальном времени
Потратьте некоторое время, чтобы изучить эту спецификацию компонента как пример и усвоить эти основы тестирования компонентов.
Автоматическое обнаружение изменений
Тесты BannerComponent часто вызывают detectChanges. Некоторые тестировщики предпочитают, чтобы среда тестирования Angular автоматически выполняла обнаружение изменений.
Это возможно, настроив TestBed с поставщиком ComponentFixtureAutoDetect. Сначала импортируйте его из библиотеки утилит тестирования:
src/app/banner.component.detect-changes.spec.ts (import)
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
Затем добавьте его в массив providers конфигурации модуля тестирования:
src/app/banner.component.detect-changes.spec.ts (AutoDetect)
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
Вот три теста, которые иллюстрируют, как работает автоматическое обнаружение изменений.
src/app/banner.component.detect-changes.spec.ts (AutoDetect Tests)
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(el.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
expect(el.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(el.textContent).toContain(comp.title);
});
Первый тест демонстрирует преимущества автоматического обнаружения изменений.
Второй и третий тесты раскрывают важное ограничение. Среда тестирования Angular не знает, что тест изменил title компонента. Сервис ComponentFixtureAutoDetect реагирует на асинхронные действия, такие как разрешение обещаний, таймеры и события DOM. Но непосредственное синхронное обновление свойства компонента невидимо. Тест должен вручную вызвать fixture.detectChanges() для запуска другого цикла обнаружения изменений.
Вместо того, чтобы гадать, когда фикстура теста будет или не будет выполнять обнаружение изменений, примеры в этом руководстве всегда вызывают
detectChanges()явным образом. ВызовdetectChanges()чаще, чем строго необходимо, не нанесёт вреда.
Тестирование компонента с внешним шаблоном
Фактический BannerComponent приложения ведет себя так же, как и предыдущая версия, но реализован по-другому. Он имеет внешние файлы шаблона и css, указанные в свойствах templateUrl и styleUrls.
src/app/banner.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.css']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
Это проблема для тестов. Метод TestBed.createComponent является синхронным. Но компилятор шаблонов Angular должен прочитать внешние файлы из файловой системы, прежде чем сможет создать экземпляр компонента. Это асинхронное действие. Предыдущая настройка для тестирования компонента со встроенным шаблоном не сработает для компонента с внешним шаблоном.
Первый асинхронный beforeEach
Настройка теста для BannerComponent должна дать компилятору шаблонов Angular время для чтения файлов. Логика в beforeEach предыдущего примера разделена на два вызова beforeEach. Первый вызов beforeEach обрабатывает асинхронную компиляцию.
src/app/banner.component.spec.ts (first beforeEach)
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
})
.compileComponents(); // compile template and css
}));
Обратите внимание на функцию async, вызываемую в качестве аргумента для beforeEach. Функция async — одна из утилит тестирования Angular и должна быть импортирована.
import { async } from '@angular/core/testing';
Она принимает безаргументную функцию и возвращает функцию, которая становится истинным аргументом для beforeEach.
Тело аргумента async выглядит очень похоже на тело синхронного beforeEach . В нём нет ничего очевидно асинхронного. Например, она не возвращает обещание, и нет функции done для вызова, как было бы в стандартных асинхронных тестах Jasmine. Внутренне, async организует выполнение тела beforeEach в специальной асинхронной зоне тестирования, которая скрывает механику асинхронного выполнения.
Все это необходимо для вызова асинхронного метода TestBed.compileComponents.
compileComponents
Метод TestBed.configureTestingModule возвращает класс TestBed, поэтому вы можете цепочкой вызовов другие статические методы TestBed, такие как compileComponents.
Метод TestBed.compileComponents асинхронно компилирует все компоненты, настроенные в модуле тестирования. В данном примере, BannerComponent — единственный компилируемый компонент. Когда compileComponents завершается, внешние шаблоны и файлы css были «встроены», и TestBed.createComponent может создавать новые экземпляры BannerComponent синхронно.
Разработчикам WebPack не нужно вызывать
compileComponents, потому что он встраивает шаблоны и css как часть автоматизированного процесса сборки, предшествующего запуску теста.
В этом примере TestBed.compileComponents компилирует только BannerComponent. Тесты в дальнейшем руководстве объявляют несколько компонентов и некоторые спецификации импортируют целые модули приложения, которые содержат ещё больше компонентов. Любой из этих компонентов может иметь внешние шаблоны и файлы css. TestBed.compileComponents асинхронно компилирует все объявленные компоненты одновременно.
Не настраивайте
TestBedпосле вызоваcompileComponents. СделайтеcompileComponentsпоследним шагом перед вызовомTestBed.createComponentдля создания экземпляра компонента-под-тестом.
Вызов compileComponents закрывает текущий экземпляр TestBed — это дальнейшая настройка. Вы не можете вызывать больше методов настройки TestBed, ни configureTestingModule, ни какие-либо из методов override.... TestBed выдаст ошибку, если вы попытаетесь.
Второй синхронный beforeEach
Синхронный beforeEach, содержащий оставшиеся шаги настройки, следует за асинхронным beforeEach.
src/app/banner.component.spec.ts (second beforeEach)
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
Эти шаги аналогичны шагам в исходном beforeEach. Они включают создание экземпляра BannerComponent и поиск элементов для проверки.
Вы можете рассчитывать на то, что программа тестирования подождёт завершения первого асинхронного beforeEach перед вызовом второго.
Ожидание compileComponents
Метод compileComponents возвращает обещание, поэтому вы можете выполнить дополнительные задачи немедленно после его завершения. Например, вы можете переместить синхронный код во второй beforeEach в обратный вызов compileComponents().then(...) и написать только один beforeEach.
Большинство разработчиков находят это трудночитаемым. Два вызова beforeEach предпочтительнее.
Попробуйте пример живого кода
Потратьте немного времени, чтобы изучить этот пример спецификации компонента.
Запуск Quickstart seed предоставляет аналогичное тестирование его
AppComponent, как вы можете видеть в этом примере. Он также вызываетcompileComponents, хотя это необязательно, так как шаблонAppComponentвстроен.В этом нет вреда, и вы можете вызвать
compileComponentsв любом случае, если в будущем решите переработать шаблон в отдельный файл. Тесты в этом руководстве вызываютcompileComponentsтолько по мере необходимости.
Тестирование компонента с зависимостью
Компоненты часто зависят от сервисов.
Компонент WelcomeComponent отображает приветственное сообщение авторизованному пользователю. Он определяет пользователя на основе свойства введённого UserService:
src/app/welcome.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from './model';
@Component({
selector: 'app-welcome',
template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>'
})
export class WelcomeComponent implements OnInit {
welcome = '-- not initialized yet --';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
В WelcomeComponent есть логика принятия решений, которая взаимодействует с сервисом, логика, которая делает этот компонент достойным тестирования. Вот конфигурация модуля тестирования для файла спецификаций src/app/welcome.component.spec.ts:
src/app/welcome.component.spec.ts
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ] // NO! Don't provide the real service!
// Provide a test-double instead
providers: [ {provide: UserService, useValue: userServiceStub } ]
});
На этот раз помимо объявления компонента-под-тестом, конфигурация добавляет поставщика UserService в список providers . Но не настоящий UserService.
Обеспечение тестовых дублей сервиса
Компоненту-под-тестом не обязательно нужно вводить реальные сервисы. На самом деле, обычно лучше, если это будут тестовые дубли (заглушки, фейки, шпионы или моки). Цель спецификации — проверить компонент, а не сервис, и реальные сервисы могут быть проблематичными.
Введение реального UserService может стать кошмаром. Реальный сервис может запросить у пользователя учётные данные для входа и попытаться подключиться к серверу аутентификации. Эти действия могут быть трудно перехватить. Гораздо проще и безопаснее создать и зарегистрировать тестовый дубль вместо реального UserService.
Данный набор тестов предоставляет минимальную заглушку UserService , которая удовлетворяет потребностям WelcomeComponent и его тестов:
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
Получение введённых сервисов
Тестам необходим доступ к (заглушке) UserService , введённой в WelcomeComponent.
Angular имеет иерархическую систему ввода. Может быть несколько инжекторов на разных уровнях, от корневого инжектора, созданного TestBed , до дерева компонентов.
Наиболее безопасный способ получить введённый сервис, способ, который всегда работает, — это получить его из инжектора компонента-под-тестом. Инжектор компонента является свойством DebugElement фикстуры.
Инжектор WelcomeComponent
// UserService actually injected into the component userService = fixture.debugElement.injector.get(UserService);
TestBed.get
Вы можете также получить сервис из корневого инжектора с помощью TestBed.get. Это легче запомнить и менее громоздко. Но это работает только тогда, когда Angular вводит экземпляр компонента с сервисом в корневой инжектор теста. К счастью, в этом наборе тестов единственный поставщик UserService — это корневой модуль тестирования, поэтому безопасно вызвать TestBed.get следующим образом:
Инжектор TestBed
// UserService from the root injector userService = TestBed.get(UserService);
Функция-утилита
inject— ещё один способ получить один или несколько сервисов из корневого инжектора теста.Для случая использования, в котором
injectиTestBed.getне работают, см. раздел Переопределение поставщиков компонента, в котором объясняется, почему вы должны получить сервис из инжектора компонента.
Всегда получайте сервис из инжектора
Не ссылайтесь на объект userServiceStub, который предоставляется модулю тестирования в теле вашего теста. Это не работает! Экземпляр userService, введённый в компонент, — это совершенно другой объект, клон предоставленного userServiceStub.
it('stub object and injected UserService should not be the same', () => {
expect(userServiceStub === userService).toBe(false);
// Changing the stub object has no effect on the injected service
userServiceStub.isLoggedIn = false;
expect(userService.isLoggedIn).toBe(true);
});
Окончательная настройка и тесты
Вот полная beforeEach с использованием TestBed.get:
src/app/welcome.component.spec.ts
beforeEach(() => {
// stub UserService for test purposes
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
providers: [ {provide: UserService, useValue: userServiceStub } ]
});
fixture = TestBed.createComponent(WelcomeComponent);
comp = fixture.componentInstance;
// UserService from the root injector
userService = TestBed.get(UserService);
// get the "welcome" element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.welcome'));
el = de.nativeElement;
});
И вот некоторые тесты:
src/app/welcome.component.spec.ts
it('should welcome the user', () => {
fixture.detectChanges();
const content = el.textContent;
expect(content).toContain('Welcome', '"Welcome ..."');
expect(content).toContain('Test User', 'expected name');
});
it('should welcome "Bubba"', () => {
userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
fixture.detectChanges();
expect(el.textContent).toContain('Bubba');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false; // welcome message hasn't been shown yet
fixture.detectChanges();
const content = el.textContent;
expect(content).not.toContain('Welcome', 'not welcomed');
expect(content).toMatch(/log in/i, '"log in"');
});
Первый — тест на корректность; он подтверждает, что вызываемый UserService используется и работает.
Второй параметр в Jasmine
it(например,'expected name') — это необязательное дополнение. Если ожидание не выполняется, Jasmine отобразит это дополнение после сообщения об ошибке ожидания. В спецификации с несколькими ожиданиями это может помочь прояснить, что пошло не так и какое ожидание не выполнилось.
Остальные тесты подтверждают логику компонента, когда служба возвращает разные значения. Второй тест проверяет эффект изменения имени пользователя. Третий тест проверяет, что компонент отображает соответствующее сообщение, когда нет вошедшего пользователя.
Тестирование компонента с асинхронной службой
Многие службы возвращают значения асинхронно. Большинство служб данных выполняют HTTP-запрос к удалённому серверу, и ответ обязательно асинхронный.
В этом примере представление «О нас» отображает цитаты Марка Твена. TwainComponent обрабатывает отображение, делегируя запрос к серверу TwainService.
Оба находятся в папке src/app/shared, потому что автор намеревается отображать цитаты Твена на других страницах в будущем. Вот TwainComponent.
src/app/shared/twain.component.ts
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit {
intervalId: number;
quote = '...';
constructor(private twainService: TwainService) { }
ngOnInit(): void {
this.twainService.getQuote().then(quote => this.quote = quote);
}
}
Реализация TwainService не имеет отношения к данному конкретному тесту. Достаточно увидеть в ngOnInit, что twainService.getQuote возвращает промис, что означает асинхронность.
В общем случае тесты не должны обращаться к удалённым серверам. Они должны эмулировать такие обращения. Настройка в этом src/app/shared/twain.component.spec.ts показывает один способ сделать это:
src/app/shared/twain.component.spec.ts (настройка)
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [ TwainService ],
});
fixture = TestBed.createComponent(TwainComponent);
comp = fixture.componentInstance;
// TwainService actually injected into the component
twainService = fixture.debugElement.injector.get(TwainService);
// Setup spy on the `getQuote` method
spy = spyOn(twainService, 'getQuote')
.and.returnValue(Promise.resolve(testQuote));
// Get the Twain quote element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.twain'));
el = de.nativeElement;
});
Шпионирование на реальной службе
Эта настройка аналогична настройке welcome.component.spec. Но вместо создания объекта-заглушки службы она вводит реальную службу (см. модуль тестирования providers) и заменяет критический метод getQuote шпионским объектом Jasmine.
spy = spyOn(twainService, 'getQuote')
.and.returnValue(Promise.resolve(testQuote));
Шпион разработан таким образом, что любой вызов getQuote получает немедленно разрешённый промис с тестовой цитатой. Шпион обходит фактический метод getQuote и, следовательно, не обращается к серверу.
Имитация экземпляра службы и шпионаж на реальной службе — оба отличные варианта. Выберите тот, который кажется проще для текущего набора тестов. Не бойтесь менять решение.
Шпионаж на реальной службе не всегда прост, особенно когда реальная служба имеет зависимые компоненты. Вы можете одновременно имитировать и шпионить, как показано в примере ниже.
Вот тесты с комментариями, которые последуют:
src/app/shared/twain.component.spec.ts (тесты)
it('should not show quote before OnInit', () => {
expect(el.textContent).toBe('', 'nothing displayed');
expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
});
it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
expect(el.textContent).toBe('...', 'no quote yet');
expect(spy.calls.any()).toBe(true, 'getQuote called');
});
it('should show quote after getQuote promise (async)', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
});
}));
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
tick(); // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
}));
Синхронные тесты
Первые два теста являются синхронными. Благодаря шпионскому объекту они проверяют, что getQuote вызывается после первого цикла обнаружения изменений, в ходе которого Angular вызывает ngOnInit.
Ни один тест не может доказать, что значение из службы отображается. Сама цитата ещё не поступила, несмотря на то, что шпион возвращает разрешённый промис.
Этот тест должен подождать по меньшей мере один полный цикл JavaScript-движка, прежде чем значение станет доступным. Тест должен стать асинхронным.
Функция async в it
Обратите внимание на async в третьем тесте.
src/app/shared/twain.component.spec.ts (асинхронный тест)
it('should show quote after getQuote promise (async)', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
});
}));
Функция async — одно из средств тестирования Angular. Она упрощает написание асинхронных тестов, обеспечивая запуск кода тестировщика в специальной асинхронной тестовой зоне, как обсуждалось ранее, когда она вызывалась в beforeEach.
Хотя async отлично справляется с скрытием асинхронных шаблонов кода, некоторые функции, вызываемые внутри теста (например, fixture.whenStable ), по-прежнему демонстрируют своё асинхронное поведение.
Альтернатива
fakeAsync, описанная ниже, устраняет этот артефакт и обеспечивает более линейный стиль программирования.
whenStable
Тест должен подождать, пока промис getQuote разрешится в следующем цикле JavaScript-движка.
Этот тест не имеет прямого доступа к промису, возвращаемому вызовом twainService.getQuote, потому что он скрыт внутри TwainComponent.ngOnInit, и поэтому недоступен для теста, который исследует только поверхность API компонента.
К счастью, промис getQuote доступен для асинхронной тестовой зоны, которая перехватывает все промисы, выпущенные в теле вызова метода async, независимо от места их возникновения.
Метод ComponentFixture.whenStable возвращает свой собственный промис, который разрешается, когда завершается промис getQuote. Фактически, промис whenStable разрешается, когда все ожидающие асинхронные операции в этом тесте завершаются — определение «стабильности».
Затем тест возобновляется и запускает новый раунд обнаружения изменений (fixture.detectChanges), который сообщает Angular обновить DOM с цитатой. Помощник getQuote извлекает текст элемента отображения, и ожидание подтверждает, что текст соответствует тестовой цитате.
Функция fakeAsync
Четвёртый тест проверяет такое же поведение компонента другим способом.
src/app/shared/twain.component.spec.ts (тест fakeAsync)
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
tick(); // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
}));
Обратите внимание, что fakeAsync заменяет async в качестве аргумента it . Функция fakeAsync — ещё одно средство тестирования Angular.
Как и async, она принимает функцию без параметров и возвращает функцию, которая становится аргументом вызова Jasmine it.
Функция fakeAsync обеспечивает линейный стиль программирования, выполняя тело теста в специальной тестовой зоне fakeAsync.
Основное преимущество fakeAsync над async состоит в том, что тест выглядит синхронным. Нет then(...) для нарушения видимого потока управления. Возвращающий промис fixture.whenStable исчезает, его заменяет tick().
Есть ограничения. Например, вы не можете сделать вызов XHR изнутри
fakeAsync.
Функция tick
Функция tick — одно из средств тестирования Angular и компаньон к fakeAsync. Вы можете вызвать её только внутри тела fakeAsync.
Вызов tick() моделирует течение времени до тех пор, пока все ожидающие асинхронные операции не завершатся, включая разрешение промиса getQuote в данном случае.
Она ничего не возвращает. Нет промиса, за которым нужно ждать. Продолжайте с тем же кодом теста, который появился в обратном вызове whenStable.then().
Даже этот простой пример легче читать, чем третий тест. Чтобы полнее оценить улучшение, представьте серию асинхронных операций, связанных в длинной цепочке обратных вызовов промисов.
jasmine.done
Хотя функции async и fakeAsync значительно упрощают асинхронное тестирование Angular, вы по-прежнему можете использовать традиционный асинхронный метод тестирования Jasmine.
Вы по-прежнему можете передать it функцию, которая принимает done обратный вызов. Теперь вы отвечаете за цепочку промисов, обработку ошибок и вызов done в подходящий момент.
Вот версия предыдущих двух тестов с использованием done:
src/app/shared/twain.component.spec.ts (тест done)
it('should show quote after getQuote promise (done)', done => {
fixture.detectChanges();
// get the spy promise and wait for it to resolve
spy.calls.mostRecent().returnValue.then(() => {
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
done();
});
});
Хотя нет прямого доступа к промису getQuote внутри TwainComponent, у шпиона есть прямой доступ, что позволяет подождать завершения getQuote.
Написание функций тестирования с помощью done, хотя и более громоздко, чем async и fakeAsync, является жизнеспособным и иногда необходимым приёмом. Например, вы не можете вызвать async или fakeAsync при тестировании кода, связанного с intervalTimer, как это часто бывает при тестировании асинхронных Observable методов.
Тестирование компонента с входными и выходными данными
Компонент с входными и выходными данными обычно появляется внутри шаблона представления компонента-хоста. Хост использует привязку свойства для установки свойства ввода и привязку события для прослушивания событий, генерируемых свойством вывода.
Цель тестирования — проверить, что такие привязки работают как ожидается. Тесты должны устанавливать входные значения и прослушивать выходные события.
DashboardHeroComponent — это небольшой пример компонента в такой роли. Он отображает отдельного героя, предоставляемого DashboardComponent. Щелчок по этому герою сообщает DashboardComponent о том, что пользователь выбрал героя.
DashboardHeroComponent встроено в шаблон DashboardComponent следующим образом:
src/app/dashboard/dashboard.component.html (выдержка)
<dashboard-hero *ngFor="let hero of heroes" class="col-1-4" [hero]=hero (selected)="gotoDetail($event)" > </dashboard-hero>
DashboardHeroComponent отображается в цикле *ngFor, который устанавливает свойство ввода каждого компонента hero на значение цикла и прослушивает событие selected компонента.
Вот определение компонента:
src/app/dashboard/dashboard-hero.component.ts (компонент)
@Component({
selector: 'dashboard-hero',
templateUrl: './dashboard-hero.component.html',
styleUrls: [ './dashboard-hero.component.css' ]
})
export class DashboardHeroComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
Хотя тестирование такого простого компонента имеет небольшую внутреннюю ценность, полезно знать как это делается. Можно использовать один из этих подходов:
- Протестировать его в использовании
DashboardComponent. - Протестировать его как самостоятельный компонент.
- Протестировать его, используя замену для
DashboardComponent.
Быстрый взгляд на конструктор DashboardComponent не рекомендует первый подход:
src/app/dashboard/dashboard.component.ts (конструктор)
constructor(
private router: Router,
private heroService: HeroService) {
}
Компонент DashboardComponent зависит от маршрутизатора Angular и HeroService. Скорее всего, вам придётся заменить их оба тестовыми двойниками, что потребует значительных усилий. Маршрутизатор кажется особенно сложным.
В разделе ниже рассматривается тестирование компонентов, которые требуют маршрутизации.
Непосредственной целью является тестирование DashboardHeroComponent, а не DashboardComponent, поэтому попробуйте второй и третий варианты.
Тестирование DashboardHeroComponent автономно
Вот настройка файла спецификаций.
src/app/dashboard/dashboard-hero.component.spec.ts (настройка)
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
})
.compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(DashboardHeroComponent);
comp = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element
// pretend that it was wired to something that supplied a hero
expectedHero = new Hero(42, 'Test Name');
comp.hero = expectedHero;
fixture.detectChanges(); // trigger initial data binding
});
Асинхронное beforeEach обсуждалось выше. После асинхронной компиляции компонентов с помощью compileComponents, остальная настройка выполняется синхронно во второй beforeEach, используя базовые техники, описанные ранее.
Обратите внимание, как код настройки присваивает тестового героя (expectedHero) свойству компонента hero, эмулируя способ, которым DashboardComponent устанавливает его через привязку свойства в своём повторителе.
Следующий тест:
src/app/dashboard/dashboard-hero.component.spec.ts (тест имени)
it('should display hero name', () => {
const expectedPipedName = expectedHero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
});
Он проверяет, что имя героя передаётся в шаблон с привязкой. Поскольку шаблон пропускает имя героя через Angular UpperCasePipe, тест должен сопоставлять значение элемента с прописным именем:
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>
Этот небольшой тест демонстрирует, как тесты Angular могут проверять визуальное представление компонента — чего невозможно сделать с помощью изолированных юнит-тестов — с низкой стоимостью и без обращения к значительно более медленным и сложным end-to-end тестам.
Второй тест проверяет поведение клика. При нажатии на героя должен быть поднят selected событие, которое может услышать компонент-хост (DashboardComponent предположительно):
src/app/dashboard/dashboard-hero.component.spec.ts (тест клика)
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
heroEl.triggerEventHandler('click', null);
expect(selectedHero).toBe(expectedHero);
});
Компонент экспонирует свойство EventEmitter. Тест подписывается на него так же, как это сделал бы компонент-хост.
heroEl — это DebugElement, который представляет героя <div>. Тест вызывает triggerEventHandler с именем события «клик». Привязка события «клик» отвечает вызовом DashboardHeroComponent.click().
Если компонент ведёт себя так, как ожидается, click() сообщает свойству компонента selected об отправке объекта hero, тест распознает это значение через подписку на selected, и тест должен пройти.
triggerEventHandler
Компонент Angular DebugElement.triggerEventHandler может генерировать любое событие, связанное с данными, по его имени. Второй параметр — объект события, передаваемый обработчику.
В этом примере тест вызывает событие «клик» с нулевым объектом события.
heroEl.triggerEventHandler('click', null);
Тест предполагает (в данном случае правильно), что обработчик событий во время выполнения — метод компонента click() — не заботится об объекте события.
Другие обработчики менее снисходительны. Например, директива RouterLink ожидает объекта со свойством button, которое идентифицирует нажатую кнопку мыши. Эта директива выбросит ошибку, если объект события не сделает это правильно.
Нажатие на кнопку, ссылку или произвольный HTML-элемент — распространённая задача при тестировании.
Упростите это, упаковав процесс триггеринга клика в вспомогательную функцию, такую как click ниже:
testing/index.ts (вспомогательная функция для клика)
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
export const ButtonClickEvents = {
left: { button: 0 },
right: { button: 2 }
};
/** Simulate element click. Defaults to mouse left-button click event. */
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
if (el instanceof HTMLElement) {
el.click();
} else {
el.triggerEventHandler('click', eventObj);
}
}
Первый параметр — элемент для нажатия. При необходимости можно передать пользовательский объект события в качестве второго параметра. По умолчанию используется (частичный) объект события нажатия левой кнопкой мыши объект события, который принимают многие обработчики, включая директиву RouterLink.
Вспомогательная функция click() не является одним из инструментов тестирования Angular. Это функция, определённая в примере кода этого руководства. Все примеры тестов её используют. Если вам нравится, добавьте её в свою собственную коллекцию вспомогательных функций.
Вот предыдущий тест, переписанный с использованием этой вспомогательной функции.
src/app/dashboard/dashboard-hero.component.spec.ts (пересмотренный тест клика)
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
click(heroEl); // triggerEventHandler helper
expect(selectedHero).toBe(expectedHero);
});
Тестирование компонента внутри компонента-хоста для тестов
В предыдущем подходе сами тесты играли роль компонента-хоста DashboardComponent. Но работает ли DashboardHeroComponent правильно, когда корректно привязан к компоненту-хосту?
Тестирование с фактическим компонентом-хостом выполнимо, но кажется более проблематичным, чем того стоит. Проще эмулировать компонент-хост с помощью тестового хоста, как этот:
src/app/dashboard/dashboard-hero.component.spec.ts (тестовый хост)
@Component({
template: `
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
})
class TestHostComponent {
hero = new Hero(42, 'Test Name');
selectedHero: Hero;
onSelected(hero: Hero) { this.selectedHero = hero; }
}
Тестовый хост привязывается к DashboardHeroComponent так же, как и DashboardComponent, но без отвлекающих факторов Router, HeroService или даже повторителя *ngFor.
Тестовый хост устанавливает свойство ввода компонента hero с помощью тестового героя. Он привязывает событие компонента selected к своему обработчику onSelected, который записывает отправленный героя в своём свойстве selectedHero. Позже тесты проверяют это свойство, чтобы убедиться, что событие DashboardHeroComponent.selected отправило правильного героя.
Настройка тестов для компонента-хоста аналогична настройке автономных тестов:
src/app/dashboard/dashboard-hero.component.spec.ts (настройка тестового хоста)
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
}).compileComponents();
}));
beforeEach(() => {
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
fixture.detectChanges(); // trigger initial data binding
});
Эта конфигурация модуля тестирования показывает два важных отличия:
- Она объявляет как
DashboardHeroComponent, так иTestHostComponent. - Она создаёт
TestHostComponentвместоDashboardHeroComponent.
createComponent возвращает fixture, содержащий экземпляр TestHostComponent, а не экземпляр DashboardHeroComponent.
Создание TestHostComponent является побочным эффектом создания DashboardHeroComponent, потому что последний появляется в шаблоне первого. Запрос элемента героя (heroEl всё ещё находит его в тестовом DOM, хотя и на большей глубине в дереве элементов, чем раньше.
Сами тесты почти идентичны автономному варианту:
src/app/dashboard/dashboard-hero.component.spec.ts (тестовый хост)
it('should display hero name', () => {
const expectedPipedName = testHost.hero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked', () => {
click(heroEl);
// selected hero should be the same data bound hero
expect(testHost.selectedHero).toBe(testHost.hero);
});
Только тест выбранного события отличается. Он подтверждает, что выбранный DashboardHeroComponent герой действительно проходит через привязку события к компоненту-хосту.
Тестирование компонента с маршрутизацией
Тестирование фактического DashboardComponent казалось сложной задачей, поскольку оно вводит Router.
src/app/dashboard/dashboard.component.ts (конструктор)
constructor(
private router: Router,
private heroService: HeroService) {
}
Также вводится HeroService, но имитация этого — знакомая история. Router имеет сложный API и переплетается с другими сервисами и предварительными условиями приложения.
К счастью, DashboardComponent не делает многого с Router.
src/app/dashboard/dashboard.component.ts (goToDetail)
gotoDetail(hero: Hero) {
let url = `/heroes/${hero.id}`;
this.router.navigateByUrl(url);
}
Часто бывает так. Как правило, вы тестируете компонент, а не маршрутизатор, и заботитесь только о том, переходит ли компонент по правильному адресу при заданных условиях. Заглушка маршрутизатора с тестовой реализацией — простой вариант. Это должно сработать:
src/app/dashboard/dashboard.component.spec.ts (заглушка Router)
class RouterStub {
navigateByUrl(url: string) { return url; }
}
Теперь настройте модуль тестирования с тестовыми заглушками для Router и HeroService, и создайте тестовый экземпляр DashboardComponent для последующего тестирования.
src/app/dashboard/dashboard.component.spec.ts (компиляция и создание)
beforeEach( async(() => {
TestBed.configureTestingModule({
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub }
]
})
.compileComponents().then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
});
Следующий тест нажимает на отображённого героя и подтверждает (с помощью шпиона), что Router.navigateByUrl вызывается с ожидаемым URL.
src/app/dashboard/dashboard.component.spec.ts (тест перехода)
it('should tell ROUTER to navigate when hero clicked',
inject([Router], (router: Router) => { // ...
const spy = spyOn(router, 'navigateByUrl');
heroClick(); // trigger click on first inner <div class="hero">
// args passed to router.navigateByUrl()
const navArgs = spy.calls.first().args[0];
// expecting to navigate to id of the component's first hero
const id = comp.heroes[0].id;
expect(navArgs).toBe('/heroes/' + id,
'should nav to HeroDetail for first hero');
}));
Функция inject
Обратите внимание на функцию inject во втором it аргументе.
it('should tell ROUTER to navigate when hero clicked',
inject([Router], (router: Router) => { // ...
}));
Функция inject — один из инструментов тестирования Angular. Она вводит сервисы в тестовую функцию, где вы можете их изменять, просматривать и манипулировать ими.
Функция inject имеет два параметра:
- Массив токенов инъекции зависимостей Angular.
- Функция теста, параметры которой точно соответствуют каждому элементу в массиве токенов инъекции.
Функция inject использует текущий инжектор TestBed и может возвращать только сервисы, предоставленные на этом уровне. Она не возвращает сервисы из поставщиков компонентов.
Этот пример вводит Router из текущего инжектора TestBed. Это нормально для данного теста, так как Router предоставляется и должен предоставляться инжектором корня приложения.
Если вам нужен сервис, предоставленный инжектором собственного компонента, используйте fixture.debugElement.injector.get:
Инжектор компонента
// UserService actually injected into the component userService = fixture.debugElement.injector.get(UserService);
Используйте собственный инжектор компонента, чтобы получить сервис, фактически введённый в компонент.
Функция inject закрывает текущий экземпляр TestBed для дальнейшей конфигурации. Вы больше не можете вызывать методы конфигурации TestBed, ни configureTestingModule, ни какой-либо из методов override.... Функция TestBed выбросит ошибку, если вы попытаетесь.
Не конфигурируйте
TestBedпосле вызоваinject.
id="routed-component-w-param">Тестирование компонента с параметрами маршрута
Нажатие на элемент Панель мониторинга вызывает перенаправление на heroes/:id, где :id — параметр маршрута, значение которого — id героя для редактирования. Этот URL соответствует маршруту к компоненту HeroDetailComponent.
Маршрутизатор помещает значение токена :id в свойство ActivatedRoute.params Observable, Angular вводит ActivatedRoute в HeroDetailComponent, и компонент извлекает id, чтобы получить соответствующего героя через HeroDetailService. Вот конструктор HeroDetailComponent:
src/app/hero/hero-detail.component.ts (конструктор)
constructor(
private heroDetailService: HeroDetailService,
private route: ActivatedRoute,
private router: Router) {
}
HeroDetailComponent подписывается на изменения ActivatedRoute.params в методе ngOnInit.
src/app/hero/hero-detail.component.ts (ngOnInit)
ngOnInit(): void {
// get hero when `id` param changes
this.route.params.subscribe(p => this.getHero(p && p['id']));
}
Выражение после
route.paramsцепляет оператор Observable, который извлекаетidизparams, а затем цепляет операторforEach, чтобы подписаться на измененияid. Значениеidменяется каждый раз, когда пользователь переходит к другому герою.
forEachпередает новое значениеidв методgetHeroкомпонента (не показано), который извлекает героя и устанавливает свойствоheroкомпонента. Если параметрidотсутствует, операторpluckзавершается с ошибкой, иcatchобрабатывает ошибку как запрос на редактирование нового героя.Руководство по маршрутизатору содержит более подробную информацию о параметрах маршрута.
Тест может исследовать, как HeroDetailComponent реагирует на разные значения параметра id путём манипулирования ActivatedRoute , введённым в конструктор компонента.
Теперь вы знаете, как подменить Router и службу данных. Подмена ActivatedRoute следует той же схеме, за исключением осложнения: ActivatedRoute.params — Observable.
id="stub-observable">Создание тестовой заглушки Observable
hero-detail.component.spec.ts полагается на ActivatedRouteStub для установки значений ActivatedRoute.params для каждого теста. Это переиспользуемый класс-помощник для тестов, применяемый во всём приложении. Рассмотрите возможность размещения таких помощников в папке testing, соседней с папкой app. В этом примере ActivatedRouteStub содержится в testing/router-stubs.ts:
testing/router-stubs.ts (ActivatedRouteStub)
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class ActivatedRouteStub {
// ActivatedRoute.params is Observable
private subject = new BehaviorSubject(this.testParams);
params = this.subject.asObservable();
// Test parameters
private _testParams: {};
get testParams() { return this._testParams; }
set testParams(params: {}) {
this._testParams = params;
this.subject.next(params);
}
// ActivatedRoute.snapshot.params
get snapshot() {
return { params: this.testParams };
}
}
Отличительные особенности этой заглушки:
-
Заглушка реализует только две функции
ActivatedRoute:paramsиsnapshot.params. -
BehaviorSubject управляет Observable-заглушкой
paramsи возвращает то же значение каждому подписчикуparamsдо тех пор, пока ему не будет присвоено новое значение. -
HeroDetailComponentцепляет свои выражения к этому Observable-заглушкеparams, который теперь под контролем тестировщика. -
Установка свойства
testParamsзаставляетsubjectпоместить назначенное значение вparams. Это запускает подписку наHeroDetailComponentparams, описанную выше, точно так же, как и при навигации. -
Установка свойства
testParamsтакже обновляет внутреннее значение заглушки для свойстваsnapshotдля возврата.
Снимок — ещё один популярный способ для компонентов потреблять параметры маршрута.
Заглушки маршрутизатора в данном руководстве предназначены для вдохновения. Создавайте свои собственные заглушки, чтобы удовлетворить свои потребности в тестировании.
id="tests-w-observable-double">Тестирование с заглушкой Observable
Вот тест, демонстрирующий поведение компонента, когда наблюдаемое id ссылается на существующего героя:
src/app/hero/hero-detail.component.spec.ts (существующий id)
describe('when navigate to existing hero', () => {
let expectedHero: Hero;
beforeEach( async(() => {
expectedHero = firstHero;
activatedRoute.testParams = { id: expectedHero.id };
createComponent();
}));
it('should display that hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
});
Метод
createComponentи объектpageрассматриваются в следующей секции. Сейчас полагайтесь на своё интуитивное понимание.
Если id не найден, компонент должен перейти на HeroListComponent. Настройка тестового набора предоставила ту же заглушку RouterStub описанную выше, которая подсматривает за маршрутизатором, но не выполняет фактическую навигацию. Этот тест предоставляет «плохой» id и ожидает, что компонент попытается перейти.
src/app/hero/hero-detail.component.spec.ts (плохой id)
describe('when navigate to non-existant hero id', () => {
beforeEach( async(() => {
activatedRoute.testParams = { id: 99999 };
createComponent();
}));
it('should try to navigate back to hero list', () => {
expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
});
});
Хотя в этом приложении нет маршрута к HeroDetailComponent без параметра id, возможно, такой маршрут будет добавлен в будущем. Компонент должен действовать разумно, когда параметра id нет.
В этой реализации компонент должен создать и отобразить нового героя. У новых героев id=0 и пустое name. Этот тест подтверждает, что компонент ведет себя ожидаемым образом:
src/app/hero/hero-detail.component.spec.ts (нет id)
describe('when navigate with no hero id', () => {
beforeEach( async( createComponent ));
it('should have hero.id === 0', () => {
expect(comp.hero.id).toBe(0);
});
it('should display empty hero name', () => {
expect(page.nameDisplay.textContent).toBe('');
});
});
Просмотрите и загрузите весь тестовый код приложения для руководства с этим живым примером.
id="page-object">Использование объекта страницы для упрощения настройки
HeroDetailComponent — это простой вид с заголовком, двумя полями героя и двумя кнопками.
Но в шаблоне уже достаточно сложности.
src/app/hero/hero-detail.component.html
<div *ngIf="hero">
<h2><span>{{hero.name | titlecase}}</span> Details</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label for="name">name: </label>
<input id="name" [(ngModel)]="hero.name" placeholder="name" />
</div>
<button (click)="save()">Save</button>
<button (click)="cancel()">Cancel</button>
</div>
Для полного тестирования компонента потребуется много настроек:
- Необходимо дождаться прибытия героя, прежде чем
*ngIfпозволит любому элементу в DOM. - Требуются ссылки на заголовок
<span>и имя<input>, чтобы можно было проверить их значения. - Требуются ссылки на две кнопки, чтобы можно было их нажать.
- Требуются шпионы для некоторых методов компонента и маршрутизатора.
Даже такая небольшая форма может привести к беспорядочной условной настройке и выбору элементов CSS.
Устраните хаос с классом Page , который упрощает доступ к свойствам компонента и инкапсулирует логику, которая их устанавливает. Вот класс Page для hero-detail.component.spec.ts
src/app/hero/hero-detail.component.spec.ts (Page)
class Page {
gotoSpy: jasmine.Spy;
navSpy: jasmine.Spy;
saveBtn: DebugElement;
cancelBtn: DebugElement;
nameDisplay: HTMLElement;
nameInput: HTMLInputElement;
constructor() {
const router = TestBed.get(Router); // get router from root injector
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
this.navSpy = spyOn(router, 'navigate');
}
/** Add page elements after hero arrives */
addPageElements() {
if (comp.hero) {
// have a hero so these elements are now in the DOM
const buttons = fixture.debugElement.queryAll(By.css('button'));
this.saveBtn = buttons[0];
this.cancelBtn = buttons[1];
this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement;
this.nameInput = fixture.debugElement.query(By.css('input')).nativeElement;
}
}
}
Теперь важные крючки для манипулирования компонентом и проверки данных организованы и доступны из экземпляра Page.
Метод createComponent создаёт объект page и заполняет пробелы после прибытия hero.
src/app/hero/hero-detail.component.spec.ts (createComponent)
/** Create the HeroDetailComponent, initialize it, set test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroDetailComponent);
comp = fixture.componentInstance;
page = new Page();
// 1st change detection triggers ngOnInit which gets a hero
fixture.detectChanges();
return fixture.whenStable().then(() => {
// 2nd change detection displays the async-fetched hero
fixture.detectChanges();
page.addPageElements();
});
}
Тесты с Observable в предыдущем разделе демонстрируют, как createComponent и page сохраняют тесты короткими и по делу. Нет отвлекающих факторов: нет ожидания разрешения обещаний и нет поиска в DOM значений элементов для сравнения.
Вот несколько дополнительных тестов HeroDetailComponent для закрепления этой концепции.
src/app/hero/hero-detail.component.spec.ts (выбранные тесты)
it('should display that hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
it('should navigate when click cancel', () => {
click(page.cancelBtn);
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save but not navigate immediately', () => {
// Get service injected into component and spy on its`saveHero` method.
// It delegates to fake `HeroService.updateHero` which delivers a safe test result.
const hds = fixture.debugElement.injector.get(HeroDetailService);
const saveSpy = spyOn(hds, 'saveHero').and.callThrough();
click(page.saveBtn);
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called');
});
it('should navigate when click save and save resolves', fakeAsync(() => {
click(page.saveBtn);
tick(); // wait for async save to complete
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
}));
it('should convert hero name to Title Case', () => {
const inputName = 'quick BROWN fox';
const titleCaseName = 'Quick Brown Fox';
// simulate user entering new name into the input box
page.nameInput.value = inputName;
// dispatch a DOM event so that Angular learns of input value change.
page.nameInput.dispatchEvent(newEvent('input'));
// Tell Angular to update the output span through the title pipe
fixture.detectChanges();
expect(page.nameDisplay.textContent).toBe(titleCaseName);
});
id="import-module">Настройка с импортом модулей
Предыдущие тесты компонента настраивали тестовый модуль с некоторыми declarations таким образом:
src/app/dashboard/dashboard-hero.component.spec.ts (настройка)
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
})
.compileComponents(); // compile template and css
}));
DashboardComponent прост. Ему не нужна помощь. Но более сложные компоненты часто зависят от других компонентов, директив, конвейеров и поставщиков, и их тоже нужно добавлять в тестовый модуль.
К счастью, параметр TestBed.configureTestingModule параллелен данным, передаваемым декоратору @NgModule, что означает, что вы также можете указать providers и imports.
HeroDetailComponent нуждается в большой помощи, несмотря на свой небольшой размер и простоту конструкции. Помимо поддержки, которую он получает от стандартного тестового модуля CommonModule, ему нужны:
-
NgModelи аналогичные элементы вFormsModuleдля активации двусторонней привязки данных. TitleCasePipeиз папкиshared.- Услуги маршрутизатора (которые в этих тестах заглушены).
- Услуги доступа к данным героев (также заглушены).
Один из подходов заключается в настройке тестового модуля из отдельных компонентов, как в этом примере:
src/app/hero/hero-detail.component.spec.ts (настройка FormsModule)
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroDetailComponent, TitleCasePipe ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
}));
Поскольку многие компоненты приложения нуждаются в FormsModule и TitleCasePipe, разработчик создал SharedModule для объединения этих и других часто запрашиваемых компонентов. Тестовую настройку можно использовать и с SharedModule , как показано в этой альтернативной настройке:
src/app/hero/hero-detail.component.spec.ts (настройка SharedModule)
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [ HeroDetailComponent ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
}));
Это немного более компактный и небольшой код с меньшим количеством операторов импорта (не показано).
id="feature-module-import">Импорт функционального модуля
HeroDetailComponent входит в состав HeroModule Функциональный модуль, который агрегирует больше взаимосвязанных компонентов, включая SharedModule. Попробуйте настроить тест, который импортирует HeroModule:
src/app/hero/hero-detail.component.spec.ts (HeroModule setup)
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ HeroModule ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
}));
Это действительно лаконично. Остались только заглушки в providers. Даже объявление HeroDetailComponent исчезло.
На самом деле, если вы попытаетесь его объявить, Angular выдаст ошибку, потому что
HeroDetailComponentобъявлен и вHeroModule, и вDynamicTestModule(модуле тестирования).
Импорт модуля компонента часто является простейшим способом настройки тестов, особенно когда модуль небольшой и в основном самодостаточный, как и должны быть модули компонентов.
Переопределение поставщиков компонента
HeroDetailComponent предоставляет собственные HeroDetailService.
src/app/hero/hero-detail.component.ts (prototype)
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css' ],
providers: [ HeroDetailService ]
})
export class HeroDetailComponent implements OnInit {
constructor(
private heroDetailService: HeroDetailService,
private route: ActivatedRoute,
private router: Router) {
}
}
Невозможно подменить поставщики компонента в providers модуля TestBed.configureTestingModule. Это поставщики для модуля тестирования, а не компонента. Они готовят инжектор зависимостей на уровне фикстуры.
Angular создаёт компонент со своим собственным инжектором, который является подчинённым инжектору фикстуры. Он регистрирует поставщиков компонента (в данном случае HeroDetailService) в подчинённом инжекторе. Тест не может получить доступ к сервисам подчинённого инжектора из инжектора фикстуры. И TestBed.configureTestingModule также не может их настроить.
Angular всё это время создавал новые экземпляры реального HeroDetailService!
Эти тесты могут завершиться ошибкой или таймаутом, если
HeroDetailServiceделал собственные вызовы XHR к удалённому серверу. Возможно, удалённого сервера для вызова нет.К счастью,
HeroDetailServiceделегирует ответственность за доступ к удалённым данным введённомуHeroService.src/app/hero/hero-detail.service.ts (prototype)
@Injectable() export class HeroDetailService { constructor(private heroService: HeroService) { } /* . . . */ }Предыдущая настройка теста заменяет реальный
HeroServiceнаFakeHeroService, который перехватывает запросы сервера и подменяет их ответы.
Что делать, если вам не так повезло. Что делать, если подмена HeroService сложна? Что делать, если HeroDetailService делает собственные запросы к серверу?
Метод TestBed.overrideComponent может заменить поставщиков компонента на легко управляемые заглушки, как показано в следующем варианте настройки:
src/app/hero/hero-detail.component.spec.ts (Override setup)
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ HeroModule ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: Router, useClass: RouterStub},
]
})
// Override component's own provider
.overrideComponent(HeroDetailComponent, {
set: {
providers: [
{ provide: HeroDetailService, useClass: HeroDetailServiceSpy }
]
}
})
.compileComponents();
}));
Обратите внимание, что TestBed.configureTestingModule больше не предоставляет (фиктивную) HeroService, поскольку она не требуется.
Метод overrideComponent
Фокусируемся на методе overrideComponent.
src/app/hero/hero-detail.component.spec.ts (overrideComponent)
.overrideComponent(HeroDetailComponent, {
set: {
providers: [
{ provide: HeroDetailService, useClass: HeroDetailServiceSpy }
]
}
})
Он принимает два аргумента: тип компонента для переопределения (HeroDetailComponent) и объект метаданных переопределения. Объект метаданных переопределения — это обобщённый тип, определённый следующим образом:
type MetadataOverride= { add?: T; remove?: T; set?: T; };
Объект метаданных переопределения может либо добавлять и удалять элементы в свойствах метаданных, либо полностью сбрасывать эти свойства. В данном примере сбрасываются метаданные компонента providers.
Параметр типа T, это вид метаданных, который вы передавали бы в декоратор @Component:
selector?: string; template?: string; templateUrl?: string; providers?: any[]; ...
Предоставление заглушки-шпиона (HeroDetailServiceSpy)
В данном примере массив providers компонента полностью заменяется новым массивом, содержащим HeroDetailServiceSpy.
HeroDetailServiceSpy — это заглушка реального HeroDetailService, которая имитирует все необходимые функции этого сервиса. Она не вводит и не делегирует в нижний уровень HeroService, поэтому нет необходимости предоставлять заглушку для этого.
Соответствующие тесты HeroDetailComponent будут проверять, что методы HeroDetailService вызывались, шпионя за методами сервиса. Соответственно, заглушка реализует свои методы как шпионы:
src/app/hero/hero-detail.component.spec.ts (HeroDetailServiceSpy)
class HeroDetailServiceSpy {
testHero = new Hero(42, 'Test Hero');
getHero = jasmine.createSpy('getHero').and.callFake(
() => Promise
.resolve(true)
.then(() => Object.assign({}, this.testHero))
);
saveHero = jasmine.createSpy('saveHero').and.callFake(
(hero: Hero) => Promise
.resolve(true)
.then(() => Object.assign(this.testHero, hero))
);
}
Тесты переопределения
Теперь тесты могут напрямую контролировать героя компонента, манипулируя testHero заглушки-шпиона, и подтверждать, что вызывались методы сервиса.
src/app/hero/hero-detail.component.spec.ts (override tests)
let hdsSpy: HeroDetailServiceSpy;
beforeEach( async(() => {
createComponent();
// get the component's injected HeroDetailServiceSpy
hdsSpy = fixture.debugElement.injector.get(HeroDetailService);
}));
it('should have called `getHero`', () => {
expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once');
});
it('should display stub hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);
});
it('should save stub hero change', fakeAsync(() => {
const origName = hdsSpy.testHero.name;
const newName = 'New Name';
page.nameInput.value = newName;
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
expect(comp.hero.name).toBe(newName, 'component hero has new name');
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
click(page.saveBtn);
expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once');
tick(); // wait for async save to complete
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
}));
Дополнительные переопределения
Метод TestBed.overrideComponent можно вызывать несколько раз для одного и того же или разных компонентов. TestBed предлагает аналогичные методы overrideDirective, overrideModule и overridePipe для погружения в эти классы и замены их частей.
Исследуйте возможности и комбинации самостоятельно.
Тестирование компонента RouterOutlet
AppComponent отображает маршрутизируемые компоненты в <router-outlet>. Он также отображает навигационную панель с якорями и их директивами RouterLink.
src/app/app.component.html
<app-banner></app-banner> <app-welcome></app-welcome> <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/heroes">Heroes</a> <a routerLink="/about">About</a> </nav> <router-outlet></router-outlet>
Класс компонента ничего не делает.
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent { }
Тесты компонентов могут подтвердить, что якоря правильно подключены без привлечения маршрутизатора. Посмотрите, почему это стоит делать ниже.
Подстановка ненужных компонентов
Настройка теста должна быть знакома.
src/app/app.component.spec.ts (Stub Setup)
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
BannerComponent, WelcomeStubComponent,
RouterLinkStubDirective, RouterOutletStubComponent
]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
AppComponent — это объявленный тестируемый объект.
Настройка расширяет стандартный модуль тестирования одним реальным компонентом (BannerComponent) и несколькими заглушками.
-
BannerComponentпростая и безопасна для использования в первоначальном виде. -
У реального
WelcomeComponentвведён сервис.WelcomeStubComponent— это заглушка без необходимости беспокоиться о сервисе. -
Реальный
RouterOutletсложный и легко ломается. ЗаглушкаRouterOutletStubComponent(вtesting/router-stubs.ts) безопасна и неактивна.
Заглушки компонентов необходимы. Без них компилятор Angular не распознаёт теги <app-welcome> и <router-outlet> и выдаёт ошибку.
Заглушка RouterLink
RouterLinkStubDirective существенно способствует тесту:
testing/router-stubs.ts (RouterLinkStubDirective)
@Directive({
selector: '[routerLink]',
host: {
'(click)': 'onClick()'
}
})
export class RouterLinkStubDirective {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
onClick() {
this.navigatedTo = this.linkParams;
}
}
Свойство метаданных host связывает событие клика элемента-хоста (<a>) с методом директивы onClick. URL, связанный с атрибутом [routerLink], передаётся в свойство linkParams директивы. Нажатие на якорь должно активировать метод onClick, который устанавливает свойство navigatedTo. Тесты могут проверить это свойство, чтобы подтвердить ожидаемое поведение «перехода по клику».
By.directive и инжектированные директивы
Небольшая дополнительная настройка инициирует первоначальную привязку данных и получает ссылки на навигационные ссылки:
src/app/app.component.spec.ts (test setup)
beforeEach(() => {
// trigger initial data binding
fixture.detectChanges();
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkStubDirective));
// get the attached link directive instances using the DebugElement injectors
links = linkDes
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
});
Два момента особого интереса:
-
Вы можете находить элементы по директиве, используя
By.directive, а не только по селекторам CSS. -
Вы можете использовать инжектор зависимостей компонента для получения прикреплённой директивы, потому что Angular всегда добавляет прикреплённые директивы в инжектор компонента.
Вот некоторые тесты, которые используют эту настройку:
src/app/app.component.spec.ts (selected tests)
it('can get RouterLinks from template', () => {
expect(links.length).toBe(3, 'should have 3 links');
expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard');
expect(links[1].linkParams).toBe('/heroes', '1st link should go to Heroes');
});
it('can click Heroes link in template', () => {
const heroesLinkDe = linkDes[1];
const heroesLink = links[1];
expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet');
heroesLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(heroesLink.navigatedTo).toBe('/heroes');
});
Тест «клик» в этом примере бесполезен. Он усердно пытается выглядеть полезным, но на самом деле тестирует
RouterLinkStubDirectiveвместо компонента. Это распространённая ошибка заглушек директив.В этом руководстве он выполняет определённую роль. Он демонстрирует, как найти элемент
RouterLink, нажать на него и проверить результат без включения всей маршрутизации. Этот навык может понадобиться для тестирования более сложного компонента, который изменяет отображение, пересчитывает параметры или перестраивает навигационные опции при нажатии пользователем на ссылку.
Зачем нужны эти тесты?
Тесты с заглушками RouterLink могут подтвердить, что компонент со ссылками и выходом настроен правильно, что у компонента есть нужные ссылки и что они все ведут в нужном направлении. Эти тесты не проверяют, сможет ли приложение перейти к целевому компоненту при нажатии пользователем на ссылку.
Заглушка RouterLink и RouterOutlet — это лучший вариант для таких ограниченных целей тестирования. Ссылка на реальный маршрутизатор сделает их хрупкими. Они могут завершиться ошибкой по причинам, не связанным с компонентом. Например, защитная мера навигации может предотвратить посещение HeroListComponent незарегистрированным пользователем. Это не вина AppComponent и никакие изменения в этом компоненте не исправят не пройденный тест.
Отдельная группа тестов может исследовать, выполняется ли навигация по приложению должным образом при наличии условий, влияющих на защитные меры, таких как аутентификация и авторизация пользователя.
Обновление руководства в будущем объяснит, как создавать такие тесты с помощью
RouterTestingModule.
"Поверхностные тесты компонентов" с NO_ERRORS_SCHEMA
Предыдущая настройка объявляла BannerComponent и подменяла два других компонента исключительно для предотвращения ошибки компилятора.
Без них компилятор Angular не распознаёт теги <app-banner>, <app-welcome> и <router-outlet> в шаблоне app.component.html и выдаёт ошибку.
Добавьте NO_ERRORS_SCHEMA к метаданным модуля тестирования schemas , чтобы сообщить компилятору игнорировать нераспознанные элементы и атрибуты. Вам больше не нужно объявлять нерелевантные компоненты и директивы.
Эти тесты являются поверхностными, так как они углубляются только в те компоненты, которые вы хотите протестировать.
Вот настройка, с import утверждениями, которая демонстрирует улучшенную простоту поверхностных тестов по сравнению с настройкой подстановки.
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { AppComponent } from './app.component';
import { RouterOutletStubComponent } from '../testing';
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent, RouterLinkStubDirective ],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
import { Component } from '@angular/core';
import { AppComponent } from './app.component';
import { BannerComponent } from './banner.component';
import { RouterLinkStubDirective } from '../testing';
import { RouterOutletStubComponent } from '../testing';
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
BannerComponent, WelcomeStubComponent,
RouterLinkStubDirective, RouterOutletStubComponent
]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
Единственными объявлениями являются компонент, подлежащий тестированию (AppComponent) и RouterLinkStubDirective, который активно участвует в тестах. Тесты в этом примере тесты в этом примере не изменены.
Поверхностные тесты компонентов с
NO_ERRORS_SCHEMAзначительно упрощают тестирование модулей сложных шаблонов. Однако компилятор больше не сообщает об ошибках, таких как неправильное написание или использование компонентов и директив.
Тестирование директивы атрибута
Директива атрибута изменяет поведение элемента, компонента или другой директивы. Ее название отражает способ применения директивы: как атрибут к элементу-хосту.
Директива HighlightDirective в примере приложения задает цвет фона элемента, основываясь либо на связанном данных цветом, либо на цвет по умолчанию (lightgray). Она также задает пользовательское свойство элемента (customProperty) в значение true без какой-либо причины, кроме как продемонстрировать, что это возможно.
src/app/shared/highlight.directive.ts
import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
@Directive({ selector: '[highlight]' })
/** Set backgroundColor for the attached element to highlight color
* and set the element's customProperty to true */
export class HighlightDirective implements OnChanges {
defaultColor = 'rgb(211, 211, 211)'; // lightgray
@Input('highlight') bgColor: string;
constructor(private el: ElementRef) {
el.nativeElement.style.customProperty = true;
}
ngOnChanges() {
this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
}
}
Она используется по всему приложению, возможно, наиболее просто в AboutComponent:
src/app/about.component.ts
import { Component } from '@angular/core';
@Component({
template: `
<h2 highlight="skyblue">About</h2>
<twain-quote></twain-quote>
<p>All about this sample</p>`
})
export class AboutComponent { }
Для тестирования конкретного использования HighlightDirective внутри AboutComponent требуются только вышеупомянутые методы (в частности, подход "Поверхностного теста").
src/app/about.component.spec.ts
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ AboutComponent, HighlightDirective],
schemas: [ NO_ERRORS_SCHEMA ]
})
.createComponent(AboutComponent);
fixture.detectChanges(); // initial binding
});
it('should have skyblue <h2>', () => {
const de = fixture.debugElement.query(By.css('h2'));
const bgColor = de.nativeElement.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
Однако, тестирование одного конкретного случая вряд ли позволит исследовать весь спектр возможностей директивы. Поиск и тестирование всех компонентов, использующих директиву, утомительно, нестабильно и практически не гарантирует полного охвата.
Изолированные модульные тесты могут быть полезны, но директивы атрибутов, подобные этой, как правило, манипулируют DOM. Изолированные модульные тесты не затрагивают DOM и, следовательно, не вызывают уверенности в эффективности директивы.
Лучшее решение — создание искусственного тестового компонента, демонстрирующего все способы применения директивы.
src/app/shared/highlight.directive.spec.ts (TestComponent)
@Component({
template: `
<h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>The Default (Gray)</h2>
<h2>No Highlight</h2>
<input #box [highlight]="box.value" value="cyan"/>`
})
class TestComponent { }
В случае
<input>значениеHighlightDirectiveпривязывается к имени значения цвета в поле ввода. Начальное значение — слово «cyan», которое должно быть цветом фона поля ввода.
Вот некоторые тесты этого компонента:
src/app/shared/highlight.directive.spec.ts (выбранные тесты)
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ HighlightDirective, TestComponent ]
})
.createComponent(TestComponent);
fixture.detectChanges(); // initial binding
// all elements with an attached HighlightDirective
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
// the h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});
// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});
it('should color 1st <h2> background "yellow"', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});
it('should color 2nd <h2> background w/ default color', () => {
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const bgColor = des[1].nativeElement.style.backgroundColor;
expect(bgColor).toBe(dir.defaultColor);
});
it('should bind <input> background to value color', () => {
// easier to work with nativeElement
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
// dispatch a DOM event so that Angular responds to the input value change.
input.value = 'green';
input.dispatchEvent(newEvent('input'));
fixture.detectChanges();
expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
});
it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties['customProperty']).toBeUndefined();
});
Некоторые методы заслуживают внимания:
-
Предикат
By.directive— отличный способ получить элементы, имеющие эту директиву, когда их типы элементов неизвестны. -
:notпсевдокласс вBy.css('h2:not([highlight])')помогает найти элементы<h2>, не имеющие директиву.By.css('*:not([highlight])')находит любой элемент, не имеющий директивы.
- Angular добавляет директиву в инжектор элемента, к которому она применена. Тест на цвет по умолчанию использует инжектор второго
<h2>для получения экземпляра егоHighlightDirectiveи егоdefaultColor.
Изолированные модульные тесты
Тестирование приложений с помощью утилит тестирования Angular — основная цель этого руководства.
Однако часто более продуктивно исследовать внутреннюю логику классов приложений с изолированными модульными тестами, которые не зависят от Angular. Такие тесты часто короче и проще для чтения, написания и поддержки.
Они не имеют лишнего багажа:
- Импорт из библиотек тестирования Angular.
- Настройка модуля.
- Подготовка инъекции зависимостей
providers. - Вызов
injectилиasyncилиfakeAsync.
Они следуют паттернам, знакомым всем разработчикам тестов:
- Демонстрируют стандартные, независимые от Angular методы тестирования.
- Создают экземпляры напрямую с помощью
new. - Заменяют тестовые дубликаты (заглушки, шпионы и моки) реальными зависимостями.
Хорошие разработчики пишут оба вида тестов для одной и той же части приложения, часто в одном и том же файле спецификаций. Пишите простые изолированные модульные тесты для проверки части в изоляции. Пишите Angular тесты для проверки взаимодействия части с Angular, обновления DOM и взаимодействия с остальной частью приложения.
Изолированные тесты сервисов
Сервисы — хорошие кандидаты для изолированного модульного тестирования. Вот некоторые синхронные и асинхронные модульные тесты FancyService без помощи утилит тестирования Angular.
src/app/bag/bag.no-testbed.spec.ts
// Straight Jasmine - no imports from Angular test libraries
describe('FancyService without the TestBed', () => {
let service: FancyService;
beforeEach(() => { service = new FancyService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getAsyncValue should return async value', done => {
service.getAsyncValue().then(value => {
expect(value).toBe('async value');
done();
});
});
it('#getTimeoutValue should return timeout value', done => {
service = new FancyService();
service.getTimeoutValue().then(value => {
expect(value).toBe('timeout value');
done();
});
});
it('#getObservableValue should return observable value', done => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
});
Грубая оценка количества строк показывает, что эти изолированные модульные тесты примерно на 25% меньше, чем эквивалентные тесты Angular. Это показательно, но не решающее. Преимущество заключается в сокращении сложности настройки и кода.
Сравните эти эквивалентные тесты FancyService.getTimeoutValue.
it('#getTimeoutValue should return timeout value', done => {
service = new FancyService();
service.getTimeoutValue().then(value => {
expect(value).toBe('timeout value');
done();
});
});
beforeEach(() => {
TestBed.configureTestingModule({ providers: [FancyService] });
});
it('test should wait for FancyService.getTimeoutValue',
async(inject([FancyService], (service: FancyService) => {
service.getTimeoutValue().then(
value => expect(value).toBe('timeout value')
);
})));
У них примерно одинаковое количество строк, но версия, зависящая от Angular, имеет больше подвижных частей, включая пару вспомогательных функций (async и inject). Оба подхода работают, и это не большая проблема, если вы используете утилиты тестирования Angular поблизости по другим причинам. С другой стороны, зачем усложнять простые тесты сервисов?
Выберите подход, который вам подходит.
Сервисы с зависимостями
Сервисы часто зависят от других сервисов, которые Angular вводит в конструктор. Вы можете тестировать эти сервисы без TestBed. Во многих случаях проще создать и ввести зависимости вручную.
DependentService — простой пример:
src/app/bag/bag.ts
@Injectable()
export class DependentService {
constructor(private dependentService: FancyService) { }
getValue() { return this.dependentService.getValue(); }
}
Он делегирует свой единственный метод, getValue, инжектированному FancyService.
Вот несколько способов его тестирования.
src/app/bag/bag.no-testbed.spec.ts
describe('DependentService without the TestBed', () => {
let service: DependentService;
it('#getValue should return real value by way of the real FancyService', () => {
service = new DependentService(new FancyService());
expect(service.getValue()).toBe('real value');
});
it('#getValue should return faked value by way of a fakeService', () => {
service = new DependentService(new FakeFancyService());
expect(service.getValue()).toBe('faked value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
service = new DependentService(fake as FancyService);
expect(service.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a FancyService spy', () => {
const fancy = new FancyService();
const stubValue = 'stub value';
const spy = spyOn(fancy, 'getValue').and.returnValue(stubValue);
service = new DependentService(fancy);
expect(service.getValue()).toBe(stubValue, 'service returned stub value');
expect(spy.calls.count()).toBe(1, 'stubbed method was called once');
expect(spy.calls.mostRecent().returnValue).toBe(stubValue);
});
});
В первом тесте создается FancyService с new и передается в конструктор DependentService.
Однако редко бывает так просто. Введенный сервис может быть сложно создать или контролировать. Вы можете смоделировать зависимость, использовать заглушечное значение или заглушить соответствующий метод сервиса замещающим методом, который легко контролировать.
Эти изолированные методы модульного тестирования отлично подходят для изучения внутренней логики сервиса или его простого взаимодействия с классом компонента. Используйте утилиты тестирования Angular при написании тестов, которые проверяют взаимодействие сервиса с компонентами в среде выполнения Angular.
Изолированные тесты Pipes
Pipes легко тестировать без утилит тестирования Angular.
Класс pipe имеет один метод, transform, который преобразует входное значение в преобразованное выходное значение. Реализация transform редко взаимодействует с DOM. Большинство pipes не зависят от Angular, кроме метаданных @Pipe и интерфейса.
Рассмотрим TitleCasePipe, который делает заглавными первые буквы каждого слова. Вот простое реализация с регулярным выражением.
src/app/shared/title-case.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({name: 'titlecase', pure: false})
/** Transform to Title Case: uppercase the first letter of the words in a string.*/
export class TitleCasePipe implements PipeTransform {
transform(input: string): string {
return input.length === 0 ? '' :
input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() ));
}
}
Любое использование регулярных выражений стоит тщательно тестировать. Используйте простой Jasmine для исследования ожидаемых случаев и граничных случаев.
src/app/shared/title-case.pipe.spec.ts
describe('TitleCasePipe', () => {
// This pipe is a pure, stateless function so no need for BeforeEach
let pipe = new TitleCasePipe();
it('transforms "abc" to "Abc"', () => {
expect(pipe.transform('abc')).toBe('Abc');
});
it('transforms "abc def" to "Abc Def"', () => {
expect(pipe.transform('abc def')).toBe('Abc Def');
});
// ... more tests ...
});
Пишем тесты Angular тоже
Это тесты pipe в изоляции. Они не могут сказать, работает ли TitleCasePipe должным образом, как он применяется в компонентах приложения.
Подумайте о добавлении тестов компонента, таких как этот:
src/app/hero/hero-detail.component.spec.ts (тест pipe)
it('should convert hero name to Title Case', () => {
const inputName = 'quick BROWN fox';
const titleCaseName = 'Quick Brown Fox';
// simulate user entering new name into the input box
page.nameInput.value = inputName;
// dispatch a DOM event so that Angular learns of input value change.
page.nameInput.dispatchEvent(newEvent('input'));
// Tell Angular to update the output span through the title pipe
fixture.detectChanges();
expect(page.nameDisplay.textContent).toBe(titleCaseName);
});
Изолированные тесты компонентов
Тесты компонентов обычно проверяют, как класс компонента взаимодействует со своим собственным шаблоном или с взаимодействующими компонентами. Утилиты тестирования Angular специально разработаны для упрощения таких тестов.
Рассмотрим этот ButtonComp компонент.
src/app/bag/bag.ts (ButtonComp)
@Component({
selector: 'button-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class ButtonComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
Следующий тест Angular демонстрирует, что нажатие кнопки в шаблоне приводит к обновлению сообщения на экране.
src/app/bag/bag.spec.ts (ButtonComp)
it('should support clicking a button', () => {
const fixture = TestBed.createComponent(ButtonComponent);
const btn = fixture.debugElement.query(By.css('button'));
const span = fixture.debugElement.query(By.css('span')).nativeElement;
fixture.detectChanges();
expect(span.textContent).toMatch(/is off/i, 'before click');
click(btn);
fixture.detectChanges();
expect(span.textContent).toMatch(/is on/i, 'after click');
});
Утверждения проверяют, что данные текут из одного элемента управления HTML (<button>) в компонент и из компонента в другой элемент управления HTML (<span>). Пройденный тест означает, что компонент и его шаблон подключены правильно.
Изолированные модульные тесты могут быстрее изучить компонент на уровне его API, исследовав гораздо больше условий с меньшими усилиями.
Вот набор модульных тестов, которые проверяют выходные данные компонента при различных входных данных компонента.
src/app/bag/bag.no-testbed.spec.ts (ButtonComp)
describe('ButtonComp', () => {
let comp: ButtonComponent;
beforeEach(() => comp = new ButtonComponent());
it('#isOn should be false initially', () => {
expect(comp.isOn).toBe(false);
});
it('#clicked() should set #isOn to true', () => {
comp.clicked();
expect(comp.isOn).toBe(true);
});
it('#clicked() should set #message to "is on"', () => {
comp.clicked();
expect(comp.message).toMatch(/is on/i);
});
it('#clicked() should toggle #isOn', () => {
comp.clicked();
expect(comp.isOn).toBe(true);
comp.clicked();
expect(comp.isOn).toBe(false);
});
});
Изолированные тесты компонентов предлагают большой охват тестами с меньшим кодом и практически без настройки. Это еще большее преимущество для сложных компонентов, которые могут потребовать тщательной подготовки с помощью утилит тестирования Angular.
С другой стороны, изолированные модульные тесты не могут подтвердить, что ButtonComp правильно привязан к своему шаблону или даже вообще привязан к данным. Используйте тесты Angular для этого.
API утилит тестирования Angular
В этом разделе перечислены наиболее полезные функции тестирования Angular и кратко описано их назначение.
Утилиты тестирования Angular включают TestBed, ComponentFixture, и несколько функций, которые управляют тестовой средой. Классы TestBed и ComponentFixture рассматриваются отдельно.
Вот сводка автономных функций в порядке их потенциальной полезности:
| Функция | Описание |
|---|---|
async |
Выполняет тело теста ( |
fakeAsync |
Выполняет тело теста ( |
tick |
Эмулирует течение времени и завершение ожидаемых асинхронных задач путём очистки очередей таймеров и микро-задач в заглушенной асинхронной тестовой зоне.
Принимает необязательный аргумент, который передвигает виртуальные часы вперёд на указанное количество миллисекунд, очищая асинхронные задачи, запланированные на этот интервал. См. обсуждение выше. |
inject
|
Вводит одну или несколько служб из текущего |
discardPeriodicTasks |
Когда тест В целом, тест должен завершаться без очередей задач. Если ожидаются ожидающие задания таймера, вызовите |
flushMicrotasks |
Когда тест В целом, тест должен ожидать завершения микро-задач. Если ожидаются ожидающие микро-задачи, вызовите |
ComponentFixtureAutoDetect |
Токен поставщика для службы, которая включает автоматическое обнаружение изменений. |
getTestBed |
Получает текущую инстанцию |
TestBed сводка по классу
Класс TestBed является одной из основных утилит тестирования Angular. Его API довольно обширен и может быть сложным, пока вы не изучите его по частям. Сначала ознакомьтесь с начальной частью этого руководства, чтобы понять основы, прежде чем пытаться освоить весь API.
Определение модуля, переданное configureTestingModule, является подмножеством свойств метаданных @NgModule.
type TestModuleMetadata = {
providers?: any[];
declarations?: any[];
imports?: any[];
schemas?: Array<SchemaMetadata | any[]>;
};
Каждый метод переопределения принимает MetadataOverride<T>, где T — это тип метаданных, соответствующий методу, то есть параметр @NgModule, @Component, @Directive или @Pipe.
type MetadataOverride= { add?: T; remove?: T; set?: T; };
API TestBed состоит из статических методов класса, которые либо обновляют, либо ссылаются на глобальную инстанцию TestBed.
Внутри все статические методы покрывают методы текущей среды выполнения TestBed экземпляра, который также возвращается функцией getTestBed().
Вызывайте методы TestBed внутри beforeEach(), чтобы гарантировать свежий старт перед каждым отдельным тестом.
Вот наиболее важные статические методы в порядке их потенциальной полезности.
| Методы | Описание |
|---|---|
configureTestingModule |
Тестовые заглушки ( Вызовите |
compileComponents |
Асинхронно скомпилируйте модуль тестирования после его настройки. Вы обязательно должны вызвать этот метод, если любой из компонентов модуля тестирования имеет После вызова |
createComponent |
Создайте экземпляр компонента типа |
overrideModule |
Замените метаданные для данного |
overrideComponent |
Замените метаданные для данного класса компонента, который может быть вложен глубоко в внутренний модуль. |
overrideDirective |
Замените метаданные для данного класса директивы, который может быть вложен глубоко в внутренний модуль. |
overridePipe |
Замените метаданные для данного класса трубы, который может быть вложен глубоко в внутренний модуль. |
get
|
Получите службу из текущего Функция Что, если служба является необязательной? Метод service = TestBed.get(FancyService, null); После вызова |
initTestEnvironment
|
Инициализирует тестовую среду для всего набора тестов. Тестовые заглушки ( Вы можете вызвать этот метод ровно один раз. Если вам нужно изменить этот параметр по умолчанию в середине набора тестов, сначала вызовите Укажите фабрику компилятора Angular, |
resetTestEnvironment |
Сбросьте начальную тестовую среду, включая модуль тестирования по умолчанию. |
Некоторые методы экземпляра TestBed не покрываются статическими методами класса TestBed. Они редко нужны.
Компонент ComponentFixture
TestBed.createComponent<T> создаёт экземпляр компонента T и возвращает строго типизированный ComponentFixture для этого компонента.
Свойства и методы ComponentFixture предоставляют доступ к компоненту, его представлению в DOM и аспектам его среды Angular.
Свойства ComponentFixture
Вот наиболее важные свойства для тестировщиков в порядке их потенциальной полезности.
| Свойства | Описание |
|---|---|
componentInstance |
Экземпляр класса компонента, созданный |
debugElement |
|
nativeElement |
Нативный элемент DOM в корне компонента. |
changeDetectorRef |
|
Методы ComponentFixture
Методы фикстуры заставляют Angular выполнять определённые задачи в дереве компонентов. Вызывайте эти методы, чтобы инициировать поведение Angular в ответ на моделируемые пользовательские действия.
Вот наиболее полезные методы для тестировщиков.
| Методы | Описание |
|---|---|
detectChanges |
Запустить цикл обнаружения изменений для компонента. Вызовите его для инициализации компонента (он вызывает Выполняет |
autoDetectChanges |
Установите это в значение Когда автоматическое обнаружение По умолчанию |
checkNoChanges |
Выполнить запуск обнаружения изменений, чтобы убедиться, что нет ожидаемых изменений. Бросает исключения, если они есть. |
isStable |
Если фикстура в настоящее время стабильна, возвращает |
whenStable |
Возвращает обещание, которое выполняется, когда фикстура становится стабильной. Чтобы продолжить тестирование после завершения асинхронной активности или асинхронного обнаружения изменений, привяжите это обещание. См. выше. |
destroy |
Запустить уничтожение компонента. |
DebugElement
DebugElement предоставляет важную информацию о представлении компонента в DOM.
Из DebugElement корневого компонента теста, возвращаемого fixture.debugElement, вы можете пройтись (и запросить) все элементы и поддеревья компонентов фикстуры.
Ниже приведены наиболее полезные DebugElement члены для тестировщиков в приблизительном порядке полезности:
| Член | Описание |
|---|---|
nativeElement |
Соответствующий элемент DOM в браузере (null для WebWorkers). |
query |
Вызов |
queryAll |
Вызов |
injector |
Инжектор зависимостей хоста. Например, инжектор экземпляра компонента корневого элемента. |
componentInstance |
Экземпляр компонента самого элемента, если он есть. |
context |
Объект, предоставляющий контекст родителя для этого элемента. Часто экземпляр родительского компонента, который управляет этим элементом. Когда элемент повторяется в |
children |
Непосредственные
|
parent |
Родительский |
name |
Имя тега элемента, если это элемент. |
triggerEventHandler |
Вызывает событие по его имени, если в коллекции Если событие отсутствует или есть какие-то другие проблемы, рассмотрите вызов |
listeners |
Обработчики, прикрепленные к свойствам компонента |
providerTokens |
Маркеры поиска инжектора этого компонента. Включает сам компонент плюс маркеры, которые компонент указывает в своих метаданных |
source |
Где найти этот элемент в шаблоне исходного компонента. |
references |
Словарь объектов, связанных с локальными переменными шаблона (например, |
Методы DebugElement.query(predicate) и DebugElement.queryAll(predicate) принимают предикат, который фильтрует поддерево исходного элемента для поиска соответствующих DebugElement.
Предикат — это любой метод, который принимает DebugElement и возвращает истинное значение. Следующий пример находит все DebugElements с ссылкой на локальную переменную шаблона под названием "content":
// Filter for DebugElements with a #content reference const contentRefs = el.queryAll( de => de.references['content']);
Класс Angular By имеет три статических метода для общих предикатов:
-
By.all- возвращает все элементы. -
By.css(selector)- возвращает элементы с соответствующими селекторами CSS. -
By.directive(directive)- возвращает элементы, которые Angular сопоставил с экземпляром класса директивы.
src/app/hero/hero-list.component.spec.ts
// Can find DebugElement either by css selector or by directive
const h2 = fixture.debugElement.query(By.css('h2'));
const directive = fixture.debugElement.query(By.directive(HighlightDirective));
Файлы настройки тестовой среды
Для модульного тестирования требуется некоторая конфигурация и запуск, которые захватываются в файлах настройки. Файлы настройки для этого руководства предоставляются вам, когда вы следуете инструкциям по Настройке. CLI предоставляет аналогичные файлы с той же целью.
Вот краткое описание файлов настройки данного руководства:
Подробные сведения о этих файлах и о том, как их переконфигурировать для ваших нужд, выходят за рамки данного руководства.
| Файл | Описание |
|---|---|
karma.conf.js |
Файл конфигурации karma, который указывает, какие плагины использовать, какие файлы приложения и тестов загружать, какие браузеры использовать и как сообщать результаты тестов. Он загружает три других файла настройки:
|
karma-test-shim.js |
Этот плагин подготавливает karma специально для тестовой среды Angular и запускает саму karma. В рамках этого процесса он загружает файл |
systemjs.config.js |
SystemJS загружает файлы приложения и тестов. Этот скрипт сообщает SystemJS, где найти эти файлы и как их загрузить. Это та же версия |
systemjs.config.extras.js |
Необязательный файл, который дополняет конфигурацию SystemJS в Стандартный Приведенный пример версии для этого руководства добавляет модель barrel в конфигурацию SystemJs |
systemjs.config.extras.js/** App specific SystemJS configuration */
System.config({
packages: {
// barrels
'app/model': {main:'index.js', defaultExtension:'js'},
'app/model/testing': {main:'index.js', defaultExtension:'js'}
}
});
| |
npm пакеты
Приведенные примеры тестов написаны для работы с Jasmine и karma. Две настройки «быстрого пути» добавили соответствующие пакеты Jasmine и karma в раздел devDependencies файла package.json. Они устанавливаются при выполнении npm install.
FAQ: Часто задаваемые вопросы
Почему размещать спецификации рядом с теми вещами, которые они проверяют?
Полезно размещать файлы спецификаций модульных тестов в той же папке, что и файлы исходного кода приложения, которые они проверяют:
- Такие тесты легко найти.
- Вы сразу видите, если какая-то часть вашего приложения отсутствует тесты.
- Близлежащие тесты могут показать, как работает часть в контексте.
- При перемещении исходного кода (неизбежно) вы помните о перемещении теста.
- При переименовании файла исходного кода (неизбежно) вы помните о переименовании файла теста.
Когда помещать спецификации в папку тестов?
Интеграционные спецификации приложения могут тестировать взаимодействия нескольких частей, распределенных по папкам и модулям. Они не принадлежат конкретной части, поэтому у них нет естественного места рядом с каким-либо одним файлом.
Часто лучше создать для них соответствующую папку в каталоге tests.
Конечно, спецификации, которые проверяют вспомогательные функции, должны находиться в папке test рядом с соответствующими файлами помощников.
© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/guide/testing.html