Spec-Zone.ru › Angular 2

Тестирование

Методы и практики тестирования приложения Angular.

Это руководство предоставляет советы и методы для тестирования приложений Angular. Хотя эта страница включает некоторые общие принципы и методы тестирования, основной упор делается на тестирование приложений, написанных с использованием Angular.

Содержание

  • Живые примеры
  • Введение в тестирование Angular
    • Инструменты и технологии
    • Настройка
    • Изолированные модульные тесты против утилит тестирования Angular
  • Первый тест Karma
    • Запуск с Karma
    • Отладка тестов
    • Попробуйте живой пример
  • Тестирование компонента
    • TestBed
    • createComponent
    • ComponentFixture, DebugElement и query(By.css)
    • Тесты
    • detectChanges: Обработка изменений Angular в тесте
    • Попробуйте живой пример
    • Автоматическое обнаружение изменений
  • Тестирование компонента с внешним шаблоном
    • Первый асинхронный beforeEach
    • compileComponents
    • Второй синхронный beforeEach
    • Ожидание compileComponents
    • Попробуйте живой пример
  • Тестирование компонента с зависимостью от сервиса
    • Предоставление тестовых заглушек для сервиса
    • Получение инжектированных сервисов
    • TestBed.get
    • Всегда получайте сервис из инжектора
    • Окончательная настройка и тесты
  • Тестирование компонента с асинхронным сервисом
    • Проверка реального сервиса
    • Синхронные тесты
    • Функция async в нём
    • whenStable
    • Функция fakeAsync
    • Функция tick
    • jasmine.done
  • Тестирование компонента с входными и выходными данными
    • Тестирование DashboardHeroComponent независимо
    • triggerEventHandler
  • Тестирование компонента внутри компонента-хоста теста
  • Тестирование маршрутизированного компонента
    • Функция-помощник inject
    • Тестирование маршрутизированного компонента с параметрами
    • Создание тестовой заглушки Observable
    • Тестирование с тестовой заглушкой Observable
  • Использование объекта page для упрощения настройки
  • Настройка с импортами модулей
  • Импорт модуля функции
  • Переопределение поставщиков компонента
    • Метод overrideComponent
    • Предоставление spy-stub (HeroDetailServiceSpy)
    • Тесты переопределения
    • Дополнительные переопределения
  • Тестирование компонента RouterOutlet
    • Заглушка ненужных компонентов
    • Заглушка RouterLink
    • By.directive и инжектированные директивы
    • Для чего нужны эти тесты?
  • "Поверхностные тесты компонентов" с NO_ERRORS_SCHEMA
  • Тестирование атрибутивной директивы
  • Изолированные модульные тесты
    • Сервисы
    • Сервисы с зависимостями
    • Трубки
    • Написание тестов Angular тоже
    • Компоненты
  • API утилит тестирования Angular
    • TestBed краткое описание класса
    • ComponentFixture
    • Свойства ComponentFixture
    • Методы ComponentFixture
    • DebugElement
  • Файлы настройки тестовой среды
    • npm пакеты
  • FAQ: Часто задаваемые вопросы

Это большая программа. К счастью, вы можете учиться понемногу и применять каждый урок.

Живые примеры

В этом руководстве представлены тесты образцового приложения, которое очень похоже на туториал «Обзор героев». Образцовое приложение и все тесты в этом руководстве доступны в виде живых примеров для просмотра, экспериментов и скачивания:

  • Спецификация для проверки тестовой среды.
  • Первый компонент спецификации со встроенным шаблоном.
  • Спецификация компонента с внешним шаблоном.
  • Спецификация компонента AppComponent seed-проекта QuickStart.
  • Образцовое приложение для тестирования.
  • Все спецификации, которые тестируют образцовое приложение.
  • Разнообразные дополнительные спецификации.

Введение в тестирование Angular

Эта страница проведет вас через написание тестов для изучения и подтверждения поведения приложения. Тестирование выполняет следующие действия:

  1. Защищает от изменений, нарушающих существующий код («регрессии»).

  2. Поясняет, что делает код как при целевом использовании, так и при отклоняющихся условиях.

  3. Выявляет ошибки в дизайне и реализации. Тесты освещают код с разных углов. Когда часть приложения кажется трудно тестируемой, часто корневой причиной является дефект дизайна, который нужно исправить сейчас, а не позже, когда его исправление станет дорогостоящим.

Инструменты и технологии

Вы можете писать и запускать тесты Angular с помощью различных инструментов и технологий. Это руководство описывает конкретные варианты, которые хорошо зарекомендовали себя.

Технология Назначение
Jasmine

Фреймворк для тестирования Jasmine предоставляет все необходимое для написания базовых тестов. Он поставляется с HTML-запускателем тестов, который выполняет тесты в браузере.

Утилиты тестирования Angular

Утилиты тестирования Angular создают тестовую среду для кода приложения Angular, подлежащего тестированию. Используйте их для управления и контроля частей приложения, когда они взаимодействуют *внутри* среды Angular.

Karma

Запускатель тестов Karma идеально подходит для написания и запуска модульных тестов во время разработки приложения. Он может быть неотъемлемой частью процессов разработки проекта и непрерывной интеграции. Это руководство описывает, как настроить и запустить тесты с помощью Karma.

Protractor

Используйте Protractor для написания и запуска тестов *по всему приложению* (e2e). Тесты по всему приложению исследуют приложение *так, как его видят пользователи*. В тестировании по всему приложению один процесс запускает реальное приложение, а второй процесс запускает тесты Protractor, которые имитируют поведение пользователя и проверяют, отвечает ли приложение в браузере ожидаемым образом.

Настройка

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

  1. Создайте новый проект, следуя инструкциям в разделе Настройка.

  2. Создайте новый проект с помощью Angular CLI.

Оба подхода устанавливают npm пакеты, файлы и скрипты, предварительно настроенные для приложений, созданных в соответствующих моделях. Их артефакты и процедуры различаются незначительно, но их основы одинаковы, и в коде тестов нет различий.

В этом руководстве приложение и его тесты основаны на инструкциях по настройке. Для обсуждения файлов настройки модульного тестирования см. ниже.

Изолированные модульные тесты против утилит тестирования Angular

Изолированные модульные тесты исследуют экземпляр класса в отдельности, без какой-либо зависимости от Angular или каких-либо инжектированных значений. Тестировщик создаёт тестовый экземпляр класса с new, обеспечивая тестовые заглушки для параметров конструктора по мере необходимости, а затем исследует поверхность API тестового экземпляра.

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

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

Для таких тестов необходимы **утилиты тестирования Angular**. Утилиты тестирования Angular включают класс TestBed, а также несколько вспомогательных функций из @angular/core/testing. Они являются основным предметом этого руководства, и вы узнаете о них при написании первого теста компонента ниже. Полное описание утилит тестирования Angular приведено позже в этом руководстве.

END_OF_DOCUMENT_MARKER ```

Но сначала вы должны написать фиктивный тест, чтобы проверить, что ваша тестовая среда настроена правильно и закрепить несколько основных навыков тестирования.

Первый тест 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 в разделе scripts npm package.json. Angular CLI имеет разные команды для выполнения той же задачи. Подстройте под это.

Через несколько мгновений karma откроет браузер и начнёт запись в консоль.

Karma browser

Спрячьте (не закрывайте!) браузер и сосредоточьтесь на выводе консоли, который должен выглядеть примерно так:

> 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.

Отладка тестов

Отлаживайте спецификации в браузере так же, как отлаживаете приложение.

  1. Раскрыйте окно браузера karma (скрытое ранее).
  2. Нажмите кнопку DEBUG; она откроет новую вкладку браузера и повторно запустит тесты.
  3. Откройте «Инструменты разработчика» браузера (Ctrl-Shift-I в Windows; Command-Option-I в OSX).
  4. Выберите раздел «Источники».
  5. Откройте файл теста 1st.spec.ts (Control/Command-P, затем начните вводить имя файла).
  6. Установите точку останова в тесте.
  7. Обновите браузер, и он остановится на точке останова.
Karma debugging

Попробуйте пример в реальном времени

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

END_OF_DOCUMENT_MARKER

Это возможно, настроив 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

Вспомогательная функция 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
});

Эта конфигурация модуля тестирования показывает два важных отличия:

  1. Она объявляет как DashboardHeroComponent, так и TestHostComponent.
  2. Она создаёт 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 имеет два параметра:

  1. Массив токенов инъекции зависимостей Angular.
  2. Функция теста, параметры которой точно соответствуют каждому элементу в массиве токенов инъекции.
inject использует инжектор TestBed

Функция 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. Это запускает подписку на HeroDetailComponent params, описанную выше, точно так же, как и при навигации.

  • Установка свойства 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 — это простой вид с заголовком, двумя полями героя и двумя кнопками.

HeroDetailComponent in action

Но в шаблоне уже достаточно сложности.

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);
});

Два момента особого интереса:

  1. Вы можете находить элементы по директиве, используя By.directive, а не только по селекторам CSS.

  2. Вы можете использовать инжектор зависимостей компонента для получения прикреплённой директивы, потому что 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 утверждениями, которая демонстрирует улучшенную простоту поверхностных тестов по сравнению с настройкой подстановки.

src/app/app.component.spec.ts (NO_ERRORS_SCHEMA)
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;
  });
}));
src/app/app.component.spec.ts (Заглушки)
  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 { }
HighlightDirective spec in action

В случае <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.

src/app/bag/bag.no-testbed.spec.ts (Изолированные)
it('#getTimeoutValue should return timeout value',  done => {
  service = new FancyService();
  service.getTimeoutValue().then(value => {
    expect(value).toBe('timeout value');
    done();
  });
});
src/app/bag/bag.spec.ts (с утилитами тестирования Angular)
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

Выполняет тело теста (it) или функции настройки (beforeEach) в специальной асинхронной тестовой зоне. См. обсуждение выше.

fakeAsync

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

tick

Эмулирует течение времени и завершение ожидаемых асинхронных задач путём очистки очередей таймеров и микро-задач в заглушенной асинхронной тестовой зоне.

Любознательные и внимательные читатели могут насладиться этой длинной статьёй блога "Задачи, микро-задачи, очереди и расписания".

Принимает необязательный аргумент, который передвигает виртуальные часы вперёд на указанное количество миллисекунд, очищая асинхронные задачи, запланированные на этот интервал. См. обсуждение выше.

inject

Вводит одну или несколько служб из текущего TestBed инжектора в тестовую функцию. См. выше.

discardPeriodicTasks

Когда тест fakeAsync завершается с ожидаемыми событиями таймера заданиями (очередь setTimeOut и setInterval обратных вызовов), тест завершается с ясным сообщением об ошибке.

В целом, тест должен завершаться без очередей задач. Если ожидаются ожидающие задания таймера, вызовите discardPeriodicTasks, чтобы очистить очередь задач и избежать ошибки.

flushMicrotasks

Когда тест fakeAsync завершается с ожидающими микро-задачами, такими как неразрешённые обещания, тест завершается с ясным сообщением об ошибке.

В целом, тест должен ожидать завершения микро-задач. Если ожидаются ожидающие микро-задачи, вызовите flushMicrotasks, чтобы очистить очередь микро-задач и избежать ошибки.

ComponentFixtureAutoDetect

Токен поставщика для службы, которая включает автоматическое обнаружение изменений.

getTestBed

Получает текущую инстанцию TestBed. Обычно это не нужно, поскольку статические методы класса TestBed обычно достаточны. Инстанция TestBed предоставляет несколько редко используемых членов, которые недоступны как статические методы.

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

Тестовые заглушки (karma-test-shim, browser-test-shim) устанавливают начальную тестовую среду и модуль тестирования по умолчанию. Модуль тестирования по умолчанию настроен с базовыми декларациями и некоторыми заменителями служб Angular, которые необходимы каждому тестировщику.

Вызовите configureTestingModule, чтобы уточнить конфигурацию модуля тестирования для определённого набора тестов, добавив и удалив импорты, объявления (компонентов, директив и труб) и поставщиков.

compileComponents

Асинхронно скомпилируйте модуль тестирования после его настройки. Вы обязательно должны вызвать этот метод, если любой из компонентов модуля тестирования имеет templateUrl или styleUrls, потому что получение шаблонов и файлов стилей компонентов необходимо асинхронное. См. выше.

После вызова compileComponents конфигурация TestBed замораживается на весь текущий набор тестов.

createComponent

Создайте экземпляр компонента типа T на основе текущей конфигурации TestBed. После вызова compileComponent конфигурация TestBed замораживается на весь текущий набор тестов.

overrideModule

Замените метаданные для данного NgModule. Помните, что модули могут импортировать другие модули. Метод overrideModule может глубоко проникнуть в текущий тестовый модуль, чтобы изменить один из этих внутренних модулей.

overrideComponent

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

overrideDirective

Замените метаданные для данного класса директивы, который может быть вложен глубоко в внутренний модуль.

overridePipe

Замените метаданные для данного класса трубы, который может быть вложен глубоко в внутренний модуль.

get

Получите службу из текущего TestBed инжектора.

Функция inject часто подходит для этой цели. Но inject выводит ошибку, если не может предоставить службу.

Что, если служба является необязательной?

Метод TestBed.get принимает необязательный второй параметр — объект, который возвращается, если Angular не находит поставщика (null в этом примере):

service = TestBed.get(FancyService, null);

После вызова get конфигурация TestBed замораживается на весь текущий набор тестов.

initTestEnvironment

Инициализирует тестовую среду для всего набора тестов.

Тестовые заглушки (karma-test-shim, browser-test-shim) вызывают её для вас, поэтому редко есть причина вызывать её самостоятельно.

Вы можете вызвать этот метод ровно один раз. Если вам нужно изменить этот параметр по умолчанию в середине набора тестов, сначала вызовите resetTestEnvironment.

Укажите фабрику компилятора Angular, PlatformRef, и модуль тестирования Angular по умолчанию. Альтернативы для платформ, не использующих браузер, доступны в общем виде @angular/platform-<platform_name>/testing/<platform_name>.

resetTestEnvironment

Сбросьте начальную тестовую среду, включая модуль тестирования по умолчанию.

Некоторые методы экземпляра TestBed не покрываются статическими методами класса TestBed. Они редко нужны.

Компонент ComponentFixture

TestBed.createComponent<T> создаёт экземпляр компонента T и возвращает строго типизированный ComponentFixture для этого компонента.

Свойства и методы ComponentFixture предоставляют доступ к компоненту, его представлению в DOM и аспектам его среды Angular.

Свойства ComponentFixture

Вот наиболее важные свойства для тестировщиков в порядке их потенциальной полезности.

Свойства Описание
componentInstance

Экземпляр класса компонента, созданный TestBed.createComponent.

debugElement

DebugElement связанный с корневым элементом компонента.

debugElement даёт представление о компоненте и его элементе DOM во время тестирования и отладки. Это важное свойство для тестировщиков. Самые интересные члены рассматриваются ниже.

nativeElement

Нативный элемент DOM в корне компонента.

changeDetectorRef

ChangeDetectorRef для компонента.

ChangeDetectorRef наиболее полезно при тестировании компонента, у которого есть метод ChangeDetectionStrategy.OnPush или обнаружение изменений компонента находится под вашим программным контролем.

Методы ComponentFixture

Методы фикстуры заставляют Angular выполнять определённые задачи в дереве компонентов. Вызывайте эти методы, чтобы инициировать поведение Angular в ответ на моделируемые пользовательские действия.

Вот наиболее полезные методы для тестировщиков.

Методы Описание
detectChanges

Запустить цикл обнаружения изменений для компонента.

Вызовите его для инициализации компонента (он вызывает ngOnInit) и после кода ваших тестов, измените значения свойств компонента, привязанные к данным. Angular не видит, что вы изменили personComponent.name и не обновит name привязку, пока не вызовете detectChanges.

Выполняет checkNoChanges впоследствии, чтобы убедиться, что нет циклических обновлений, если не вызывается как detectChanges(false);

autoDetectChanges

Установите это в значение true, когда вы хотите, чтобы фикстура автоматически обнаруживала изменения.

Когда автоматическое обнаружение true, тестовая фикстура вызывает detectChanges сразу после создания компонента. Затем она следит за соответствующими событиями зоны и вызывает detectChanges соответственно. Когда ваш тестовый код изменяет значения свойств компонента напрямую, вам, вероятно, все равно нужно вызвать fixture.detectChanges для запуска обновлений привязки данных.

По умолчанию false. Тестировщики, предпочитающие точный контроль над поведением тестов, обычно оставляют его false.

checkNoChanges

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

isStable

Если фикстура в настоящее время стабильна, возвращает true. Если есть асинхронные задачи, которые еще не завершились, возвращает false.

whenStable

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

Чтобы продолжить тестирование после завершения асинхронной активности или асинхронного обнаружения изменений, привяжите это обещание. См. выше.

destroy

Запустить уничтожение компонента.

DebugElement

DebugElement предоставляет важную информацию о представлении компонента в DOM.

Из DebugElement корневого компонента теста, возвращаемого fixture.debugElement, вы можете пройтись (и запросить) все элементы и поддеревья компонентов фикстуры.

Ниже приведены наиболее полезные DebugElement члены для тестировщиков в приблизительном порядке полезности:

Член Описание
nativeElement

Соответствующий элемент DOM в браузере (null для WebWorkers).

query

Вызов query(predicate: Predicate<DebugElement>) возвращает первый DebugElement, который соответствует предикату на любой глубине поддерева.

queryAll

Вызов queryAll(predicate: Predicate<DebugElement>) возвращает все DebugElements, которые соответствуют предикату на любой глубине поддерева.

injector

Инжектор зависимостей хоста. Например, инжектор экземпляра компонента корневого элемента.

componentInstance

Экземпляр компонента самого элемента, если он есть.

context

Объект, предоставляющий контекст родителя для этого элемента. Часто экземпляр родительского компонента, который управляет этим элементом.

Когда элемент повторяется в *ngFor, контекстом является NgForRow, чье свойство $implicit - значение значения экземпляра строки. Например, hero в *ngFor="let hero of heroes".

children

Непосредственные DebugElement дочерние элементы. Переходите по дереву, проходя вниз по children.

DebugElement также имеет childNodes, список DebugNode объектов. DebugElement происходит от DebugNode объектов, и часто узлов больше, чем элементов. Тестировщики обычно могут игнорировать простые узлы.

parent

Родительский DebugElement элемент. Null, если это корневой элемент.

name

Имя тега элемента, если это элемент.

triggerEventHandler

Вызывает событие по его имени, если в коллекции listeners элемента есть соответствующий обработчик. Второй параметр — объект события, ожидаемый обработчиком. См. выше.

Если событие отсутствует или есть какие-то другие проблемы, рассмотрите вызов nativeElement.dispatchEvent(eventObject).

listeners

Обработчики, прикрепленные к свойствам компонента @Output и/или свойствам событий элемента.

providerTokens

Маркеры поиска инжектора этого компонента. Включает сам компонент плюс маркеры, которые компонент указывает в своих метаданных providers.

source

Где найти этот элемент в шаблоне исходного компонента.

references

Словарь объектов, связанных с локальными переменными шаблона (например, #foo), с ключами по имени локальной переменной.

Методы 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, который указывает, какие плагины использовать, какие файлы приложения и тестов загружать, какие браузеры использовать и как сообщать результаты тестов.

Он загружает три других файла настройки:

  • systemjs.config.js
  • systemjs.config.extras.js
  • karma-test-shim.js
karma-test-shim.js

Этот плагин подготавливает karma специально для тестовой среды Angular и запускает саму karma. В рамках этого процесса он загружает файл systemjs.config.js.

systemjs.config.js

SystemJS загружает файлы приложения и тестов. Этот скрипт сообщает SystemJS, где найти эти файлы и как их загрузить. Это та же версия systemjs.config.js , которую вы установили во время настройки.

systemjs.config.extras.js

Необязательный файл, который дополняет конфигурацию SystemJS в systemjs.config.extras.js конфигурацией для конкретных потребностей самого приложения.

Стандартный systemjs.config.js не может предвидеть эти потребности. Вы заполняете пробелы здесь.

Приведенный пример версии для этого руководства добавляет модель barrel в конфигурацию SystemJs packages.

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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API