Лучшие практики модульных тестов TypeScript, часть 5: как «модульное тестирование» (почти) всего в TypeScript

Советы и методы создания модульного теста для 99,9999% кода TypeScript

Psst!

Вы можете найти часть 1 здесь: Введение,
И часть 2 здесь: IDE и настройка проекта,
И часть 3 Здесь: определения и правила
И часть 4 здесь: чистая структура тестовых наборов!

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

Для рассматриваемых здесь случаев мы использовали мокко, синон, чай и синон-чай для настройки нашей тестовой среды!

Как заглушить экспортированную функцию?

Это очень просто. Если вы следовали советам по настройке tsconfig.json, ваш модуль настроен на commonjs, и у вас не возникнет проблем с этой стратегией. Возьмем следующий код:

Чтобы имитировать otherFunction, просто импортируйте его с «* as», например:

Обратите внимание, что реальный тип возвращаемого значения otherFunction не имеет значения: важно только то, что myFunction возвращает то, что возвращает otherFunction. Это гарантировано этим тестом.

И почему это работает?

Если вы перекомпилируете свой код и посмотрите на сгенерированный JavaScript, вы увидите, что вызов otherFunction будет выглядеть примерно так:

otherFunction_1.otherFunction(123)

Итак, объект, который является результатом запроса, содержит ссылку на otherFunction. В тестовом коде, импортируя его с помощью «* as», вы упрощаете доступ к другой ссылке на такой объект и, как следствие, имеете доступ к той же ссылке, которую тестируемый код должен otherFunction. Вот и все. Секрет создания заглушек состоит в том, чтобы гарантировать, что тестовый код и тестируемый код указывают на один и тот же адрес памяти.

Как заглушить конструктор класса?

Представьте себе этот код:

Ну… что я должен здесь протестировать? Это всего лишь экземпляр класса!

Но эту функцию можно вызывать во многих местах, и во всех этих местах вы уверены, что функция заставляет ее работать правильно:

  • Создание экземпляра MyClass с правильными параметрами
  • Возврат экземпляра MyClass;

Разве не стоит следить за тем, чтобы эта работа была сделана правильно, учитывая широкое использование?

Ответ: да, всегда так. Целью является покрытие 100%, поэтому ни один код не стоит модульного тестирования.

У вас есть две стратегии:

  • Просто проверьте, является ли возвращенный экземпляр экземпляром MyClass: не очень хорошо. Что делает конструктор MyClass? Будете ли вы издеваться над всеми методами, которые вызывает этот конструктор? Как вы убедитесь, что передаются правильные параметры? Помните: вам не нужно беспокоиться о масштабах метода X при тестировании метода Y, поэтому нет, мы не рекомендуем этот вариант;
  • Конструктор Stub MyClass: Да. "Это способ".

Но мы знаем, как заглушить функции и методы, но как я могу заглушить конструктор?

Ответ довольно прост: в JavaScript класс - это не что иное, как функция, возвращающая объект на основе прототипа. Итак, вы можете издеваться над классом, как с любой другой функцией. Давайте посмотрим, как мы можем это сделать здесь:

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

Как протестировать ts-файл без функций, просто исполняемый код?

Возьмем такой пример:

Как это проверить, если у меня нет функции для моделирования обоих сценариев?

Что ж, настоящая проблема здесь в требовании кеширования. Когда файл загружается с помощью require, экспортированный результат сохраняется в его кеше. Затем при следующем вызове require того же файла код не запускается. Итак, как это решить? Конечно же, удаление кеша!

Как заглушить напрямую экспортируемые функции (module.exports = function, export = function)?

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

Возьмем такой пример:

Посмотрите, как мы импортировали функцию. Этот импорт / требование синтаксиса выглядит странно, но это рекомендуемый способ импорта зависимостей, экспортированных таким образом!

Как мы уже сказали, untestableOldFunction нельзя заглушить. Если мы импортируем его в тестовый код, ссылка на память никогда не будет прежней! Итак, как это решить? Мы можем обернуть эту ссылку в наш собственный экспорт, например:

Посмотрите, что мы сделали приведение типа функции. Мы делаем это так, чтобы неэкспортированный тип не рассматривался вне этой оболочки. Эта подпись не обязательно должна точно соответствовать untestableOldFunction, она просто должна соответствовать вашим потребностям в использовании.

Теперь мы рекомендуем написать тест для проверки обертки:

Теперь с помощью модульного теста вы гарантировали, что при использовании nowTestableOldFunction вы будете использовать untestableOldFunction, и, если кто-то изменит его в будущем, модульный тест будет предупредить его!

Теперь вам нужно изменить импорт в коде, который вы хотите протестировать:

Вот и все! Теперь у вас есть код, который можно полностью протестировать! Я даже не буду приводить здесь пример теста, потому что у вас уже есть довольно хороший пример выше!

Но важно отметить, что иногда класс может быть экспортирован непроверяемым способом. Тем не менее, можно использовать ту же стратегию, например:

Обратите внимание, что в этом примере приведение UntestableCass не является интерфейсом, представляющим его экземпляр. Почему? Как мы уже говорили, потому что класс - это просто функция, которая возвращает объект на основе прототипа. NowTestableClassType представляет тип экземпляра прототипа, а не самого класса.

Как заглушить глобальные функции?

Представьте себе следующий код:

Мы покажем довольно простой и рекомендуемый подход, когда вы имеете дело с функциями глобального времени, как в этом конкретном случае. С SinonFakeTimer вы можете имитировать поведение часов, имитируя прохождение времени и, тем самым, запускать все установленные временные события. Допустимый модульный тест для этого метода будет выглядеть так:

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

Теперь посмотрите, что setImmediate запускается только при вызове метода tick с параметром 0. tick Имитирует N миллисекунды проходят. Но как запустить setImmediate, если я говорю, что прошло 0 мс?

Как следует из названия, обратный вызов setImmediate будет оцениваться при первой возможности. На это не нужно тратить время, только оценка событий.

С другой стороны, для срабатывания обратного вызова setTimeout требуется 100 мс, поскольку он был установлен для него.
Довольно просто, не правда ли?

Аналогичную стратегию можно использовать и с другими глобальными функциями, главное: их можно заглушить!

Тестирование функций обработчика потока

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

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

Например, возьмем этот фрагмент кода:

Не рекомендуемый многословный подход

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

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

Рекомендуемый подход с использованием простых заглушек

Тем не менее, для рассматриваемого фрагмента кода более простым решением будет просто следовать рекомендациям, которые мы дали выше, и просто написать такой тест:

Почему этого достаточно?

Потому что вы тестируете streamExample, а не поведение собственного потока. Вы просто верите, что он уже протестирован поставщиками. В конце концов, это юнит-тест, а не интегрированный. Но по какой-либо причине, если вы хотите включить в свой тест собственное поведение, возможно, потому что вы не слишком доверяете поставщикам или это упростит тест, в этом нет ничего плохого, просто убедитесь, что нет доступа к внешним ресурсам, например к базе данных, файлу или API.

Когда все становится еще хуже

Может быть, функция, которую вы тестируете, вместо того, чтобы выполнять какие-то функции по конвейеру, использует события потока для реализации различного поведения, например, для преобразования результата и возврата обещания. Это довольно распространенная потребность, и она заслуживает изучения. Возьмем этот фрагмент кода:

Эта реализация довольно сложна, буквально напугана.

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

В этом сценарии обе стратегии, о которых мы говорили для потока, приведут к сложному тесту (попробуйте сами и убедитесь!). Итак ... какова самая простая стратегия тестирования этого кода? Для этого мы, наконец, воспользуемся вспомогательной библиотекой: stream-mock!

Посмотрите, насколько простым стал тест, каждый случай даже проще, чем тестируемый код.

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

Также мы проверили случай, когда в процессе возникает какая-то ошибка, что тоже немаловажно!

Источники заглушки событий

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

Самый простой способ протестировать этот код - создать EventEmitter и генерировать каждое событие в отдельных тестовых примерах:

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

Заглушка вызовов связанных методов (свободный интерфейс)

Есть несколько библиотек, которые используют обширные цепочки вызовов методов. Известно, что такие библиотеки следуют концепции, называемой свободный интерфейс. Обещания с использованием методов then и catch являются хорошим примером. Другой популярный пример - библиотека SuperAgent. Взгляните на этот фрагмент кода, извлеченный прямо с их сайта:

request
   .post('/api/pet')
   .send({ name: 'Manny', species: 'cat' })
   .set('X-API-Key', 'foobar')
   .set('Accept', 'application/json')
   .then(res => {
      alert('yay got ' + JSON.stringify(res.body));
   }); 

Такой код выглядит трудоемким для заглушки, потому что в основном нам нужна одна заглушка, возвращающая объект с заглушкой другого метода. Однако есть несколько библиотек, которые помогают протестировать этот конкретный случай, например, nock. Несмотря на это, мы хотим предложить ванильный способ, используя только мокко, синон, чай и синон-чай. , чтобы протестировать этот фрагмент кода:

Во-первых, обратите внимание, что мы использовали нашу поэтическую лицензию, чтобы изменить предупреждение для console.log. Поскольку наш подход предназначен для внутреннего тестирования, здесь нет объекта window, следовательно, нет метода alert для использования.

Кроме того, мы создали обещание, которое будет результатом публикации метода. Почему? Таким образом, метод then будет вести себя так, как должен, без заглушки. Мы могли бы заглушить тогда, но решили не делать этого. Обещания - это способ определить поток выполнения, а заглушка их методов - просто способ чрезмерно усложнить ваш тест. В этом конкретном случае, поскольку SuperAgentRequest является объектом PromiseLike, этот подход является простым способом разрешения потока.

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

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

В частности, я предпочитаю этот подход nock для модульных тестов, но это дело вкуса. Дело в том, что он работает для каждого решения Fluent like, которое вам нужно протестировать.

Но что, если порядок вызовов имеет значение?

Что ж, в этом случае мы можем использовать следующий подход:

Это так же просто и чисто, и если порядок вызовов изменится, тест прервется.

И снова: он работает не только с SuperAgent, но и со всеми подобными Fluent библиотеками.

Как протестировать код внутри анонимной функции?

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

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

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

Как это проверить, тогда? Достаточно просто:

Видеть? С помощью метода callsfake мы можем полностью имитировать поведение заглушенного метода, как нам заблагорассудится, поэтому мы заставляем его вызывать анонимную функцию, первые параметры, которые получает callbacker. Кроме того, при утверждении вызова callbacker мы использовали сопоставитель sinon, чтобы убедиться, что он получил некоторую функцию. Здесь мы не можем быть очень напористыми, поскольку у нас нет ссылки на анонимную функцию, но этого достаточно, и мы могли бы протестировать оба сценария для нашего метода!

Вот и все!

Думаю, это покрывает самые хитрые сценарии заглушки! Если у вас возникла другая ситуация, сообщите нам, и мы найдем способ ее проверить!