Есть короткий момент времени (обычно сразу после того, как Angular CLI формирует ваше новое приложение), когда приложение ведет себя синхронно и отображает некоторый статический текст в браузере. Почти как только вы начнете добавлять функции, будь то маршрутизация на новые страницы в вашем приложении или взаимодействие с серверной частью, вы теперь имеете дело с асинхронным приложением.

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

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

Но так быть не должно. В этом руководстве мы сначала рассмотрим различные стратегии, доступные для тестирования асинхронного кода Angular. Далее мы будем использовать базовые стратегии для написания тестов для компонентов с асинхронным поведением. Наконец, мы рассмотрим поведение whenStable и beforeEach(async(() => ...), двух шаблонов асинхронного тестирования, которые иногда могут сбивать с толку новых тестировщиков.

В качестве последнего замечания по темам, которые мы рассмотрим, это подходящее место для вас, если вы хотите глубже понять асинхронное тестирование для Promises и setTimeout, которые представляют две категории асинхронного поведения, известные как Микрозадачи и Макрозадачи. соответственно. Однако, если вы заинтересованы в тестировании Observables, которые также могут моделировать асинхронное поведение, следите за обновлениями, и мы расскажем о последующих публикациях.

Стратегии асинхронного тестирования (часть I)

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

Жасмин: готово

Спецификации тестов Jasmine, то есть it функций, чаще всего вызываются с использованием двух параметров. При тестировании асинхронного кода вы включаете обратный вызов done как часть анонимной функции (второй параметр), которую вы передаете программе запуска тестов Jasmine. Спецификация теста завершается, когда вызывается обратный вызов done. Если обратный вызов никогда не вызывается, тест будет выполняться до тех пор, пока он не столкнется с интервалом тайм-аута Jasmine и не завершится ошибкой.

Асинхронный режим Angular

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

Угловой fakeAsync

Последняя стратегия значительно отличается от своих аналогов тем, что происходит в смоделированное время. Перед вызовом тестового кода метод fakeAsync переопределяет объекты и поведение, используемые в Angular для управления управлением изменениями. Например, он устанавливает для времени браузера 0 эпоху и делегирует контроль над течением времени тестеру с помощью функции tick(milliseconds: number). Побочным эффектом этого элемента управления является то, что вы не можете выполнять фактические HTTP-запросы в своем тестовом коде, поскольку они будут выполняться в реальном времени. Если вам нужно сделать настоящий HTTP-запрос, вам нужно будет использовать одну из предыдущих стратегий.

Резюме

+-----------+-----------+------------------+---------------+
|   Name    |  Provider |  Time Management |      HTTP     |
+-----------+-----------+------------------+---------------+
| done      |  Jasmine  |  real            |  OK           |
| async     |  Angular  |  real            |  OK           |
| fakeAsync |  Angular  |  mock            |  Throws Error |
+-----------+-----------+------------------+---------------+

Шаблоны тестирования компонентов

Теперь, когда вы понимаете три шаблона тестирования асинхронного кода, давайте применим некоторые из тех же концепций к написанию тестов для простого компонента. Если вы хотите увидеть более полные примеры кода для остальных сценариев, обратитесь к моей странице GitHub.

Впервые я познакомился с идеей поверхностных и изолированных компонентных тестов в Замечательном руководстве Виктора Савкина (различие, которое Виктор проводит между тестами, может помочь прояснить, какие асинхронные тестовые шаблоны будут уместными). Чтобы быстро резюмировать разницу, изолированные тесты делают утверждения о поведении компонента как чистого объекта JavaScript без какого-либо HTML-рендеринга. Мелкие тесты расширяют это поведение, делая утверждения о визуализированном шаблоне компонента. Чтобы проиллюстрировать разницу между двумя стратегиями, мы протестируем результаты одного и того же UserSignupComponent.addNewUser метода в нашем демонстрационном приложении. В этом методе каждый вызов метода увеличивает счетчик регистрации пользователей на единицу (до пяти).

Тесты изолированных компонентов

Давайте сначала посмотрим, как мы могли бы написать некоторые изолированные тесты компонентов, в которых мы делаем только утверждения о состоянии чистого объекта JavaScript без какой-либо визуализации шаблона компонента. Первый тест, который мы напишем, использует оболочку Angular async для управления асинхронным кодом.

Несмотря на то, что тест проходит успешно (ура!), Такой способ написания кода все же имеет ряд недостатков. Во-первых, код не проходит естественным образом через тестовый шаблон упорядочить-действовать-утверждать; вместо этого утверждение теста похоронено в одной из первых строк кода в спецификации теста. Во-вторых, три вызова component.addNewUser() происходят синхронно, и в результате все три связанных обещания разрешатся при первом вызове .then(() => . Это усложняет вашу способность делать независимые, инкрементные утверждения о состоянии компонента при выполнении каждого обещания.

Давайте посмотрим, как использование fakeAsync метода оболочки для тестирования одного и того же блока кода может решить некоторые из этих проблем (примечание: аналогично тому, как tick делегирует контроль над течением времени тестеру, flush делегирует контроль над разрешением обещаний. предупреждения, неразрешенные обещания в конце вашей тестовой спецификации не будут вызывать исключения так же, как ожидающие задачи таймера.)

Тест все еще проходит, и вы увеличили покрытие кода до 100%, так как теперь вы тестируете поведение кода вокруг этого isMaxUser логического свойства. Кроме того, вы сгладили код и добились более естественного прохождения тестового кода, то есть ваши утверждения немедленно следуют за тестируемым кодом. Наконец, использование flush для управления разрешением обещаний улучшает вашу способность делать инкрементные утверждения о состоянии компонента.

Мелкие тесты компонентов

Обратной стороной написания только изолированных тестов является то, что вы можете написать самый многофункциональный компонент в мире, но если ваш HTML-шаблон выглядит как <div>TODO</div>, ваши конечные пользователи не получат никакой пользы от этих функций. Давайте посмотрим, как поверхностные тесты могут помочь выявить эту проблему раньше. Мы снова будем использовать UserSignupComponent.addNewUserMethod, но на этот раз мы привяжем наш HTML-шаблон к свойству компонента, то есть <h1>{{userSignupCount}}</h1>.

В качестве одной из оговорок относительно поверхностных тестов компонентов, Angular отключает автоматическое обнаружение изменений в тестовых спецификациях, чтобы предоставить тестировщикам значительный и детальный контроль над тестируемым компонентом. Однако управление обнаружением изменений вручную может создать медленную кривую обучения для разработчиков, привыкших писать компоненты приложений, в которых обнаружение изменений по умолчанию равно CheckAlways (если вы считаете, что автоматическое обнаружение изменений проще, вы можете выполнить fixture.autoDetectChanges(true) в любой области, которая имеет доступ к вашему устройству компонентов) .

Плохие новости. Ваш новый тест не работает 😞, чего и следовало ожидать, когда вы переключаетесь между контекстами с разными стратегиями обнаружения изменений по умолчанию. Давайте продолжим и добавим fixture.detectChanges(), чтобы вручную запускать обнаружение изменений (мы также будем использовать необязательный аргумент expectationFailCallback в тестовых утверждениях Жасмин, чтобы добавить некоторую документацию о наших предположениях для будущих разработчиков).

Снова пас! Хорошая работа.

Стратегии асинхронного тестирования (часть II)

В первом разделе, посвященном стратегиям асинхронного тестирования, мы рассмотрели примеры done, async и fakeAsync стратегий тестирования. Мы продолжим это исследование здесь и рассмотрим, как эти стратегии взаимодействуют с более сложными потоками управления, такими как асинхронные beforeEach функции и whenStable + isStable (последнее - шаблон, который я периодически вижу неправильно). Однако наиболее распространенным злоупотреблением паттерном является fixture.detectChanges в тесте изолированного компонента - чаще всего в этом нет необходимости, если вы не делаете утверждений о визуализированном HTML.

Когда стабильно

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

А вот документация по API:

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

И это все, что у вас есть. Нет пояснений относительно уместности fakeAsync и done тестовых стратегий. Аналогичным образом, пример кода реализует метод для поверхностного тестирования компонентов, но никогда не уточняет, подходит ли этот метод также для тестов изолированных компонентов.

Давайте посмотрим, сможем ли мы проанализировать документацию API и пролить немного света на эту практику тестирования. Документация по API оставляет нам два возможных ключа к разгадке, когда в ней используются слова fixture и event, поэтому я предлагаю свое собственное возможное объяснение того, что происходит: whenStable реагирует только на дестабилизирующие, асинхронные события на уровне fixture, которые могут объяснить, почему fixture.detectChanges, который внутренне вызывает ngOnInit и fixture.componentInstance.ngOnInit(), дает два разных результата, представленных в приведенном ниже тестовом коде:

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

+---------------+-----------------+---------------------------+
+ Fixture       | Event           | Component                 |
+---------------+-----------------+--------+------------------+
| DetectChanges | Browser (click) | OnInit | Instance method  |
+---------------+-----------------+--------+------------------+
| passes        | passes          | fails  | fails            |
+---------------+-----------------+--------+------------------+

Вернемся к исходным вопросам: подходит для fakeAsync и done? Конечно. Подходит для стратегий тестирования изолированных и неглубоких компонентов? Здесь уместность будет больше зависеть от контекста, но общее правило состоит в том, что это будет более подходящим для неглубоких тестов, чем для изолированных тестов (просто обязательно используйте fixture.componentInstance.ngOnInit в последнем случае, если ваш тестовый пример требует кода в методе OnInit для выполнения ).

Асинхронный перед каждой настройкой

Еще одно тонкое асинхронное поведение во время тестирования компонентов Angular связано с начальным beforeEach(async(() => {}), которое вы иногда видите как первый блок внутри спецификации теста. За ним часто следует второй синхронный beforeEach. Итак, что можно и чего нельзя делать при использовании async поведения в вашей тестовой среде?

Документы по тестированию Angular предоставляют немного больше контекста:

[Компонент] требует внешних файлов, которые компилятор должен прочитать из файловой системы, что по своей сути является асинхронной операцией. Если бы TestBed было разрешено продолжить, тесты запустились бы и загадочно завершились бы неудачей ...

Документация Angular API для [Test Bed] разъясняет этот момент еще более подробно:

Скомпилируйте компоненты с templateUrl для тестового модуля NgModule. Эту функцию необходимо вызывать, поскольку получение URL-адресов выполняется асинхронно.

Для этого ставки довольно низкие. Если вы вызываете .compileComponents, вероятно, лучше всего заключить его в асинхронный beforeEach блок, независимо от того, есть ли у вас в настоящее время внешний шаблон или набор стилей. Таким образом, вы обеспечите определенную стабильность, если когда-нибудь решите мигрировать в этом направлении по мере роста сложности вашего приложения.

Собираем все вместе

Вот и все! К этому моменту, я надеюсь, вы почувствуете себя более уверенно в инструментах и ​​методах тестирования асинхронного кода Angular. И если вы столкнетесь с еще одной из этих неприятных трассировок стека, помните, что вы не можете делать HTTP-запросы, которые происходят в реальном времени внутри тестовой спецификации, происходящей в ложное время.

А еще у вас есть это. Если вы все еще в тупике, вот несколько ресурсов для тестирования Angular, которые я считаю чрезвычайно полезными в качестве справочных руководств: