Представьте себе следующий сценарий: у вас есть класс TypeScript, который полагается на какой-то другой класс для выполнения своей задачи.

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

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

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

Перво-наперво

В качестве отправной точки включите в файл package.json следующие строки:

Мы будем использовать модуль npm ts-jest, чтобы Jest мог работать с нашими файлами TypeScript. Запуск npm test в вашем интерфейсе командной строки приведет к запуску тестов. Если вы попытаетесь запустить его сейчас, Jest будет жаловаться на то, что не нашел никаких тестов.

Добро пожаловать в первый класс

Давайте теперь создадим наш первый класс TS. В этом примере мы напишем класс для работы с (довольно стандартной) сущностью User традиционным для REST способом: получить всех пользователей, получить одного конкретного пользователя и так далее.

Поскольку мы отличные разработчики, сначала напишем тест:

Это довольно простой тест: мы ожидаем, что у класса Users будет метод all (), который возвращает массив пользователей.

Давайте реализуем реальный класс Users:

Мы просто возвращаем массив пользователей прямо из класса Users. Если мы запустим тесты сейчас, то получим:

Хорошо, теперь наш (очень простой) тест проходит. Пальцы вверх!

Делаем глупый код немного менее глупым

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

Итак, давайте создадим папку с именем common и файл http.ts с классом Http в нем. Этот класс будет использовать удивительные аксиомы для выполнения запросов.

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

В качестве следующего шага мы изменим исходный класс Users, чтобы использовать наш новый класс Http и получить некоторые реальные данные из нашего API:

Если мы снова запустим тесты, то получим:

Разве это не мило? Не правда ли?

Не так быстро, ковбой!

Итак, да, модульные тесты проходят, я вам это даю. Но это совсем не идеальная ситуация. Тесты не изолированы. Запрос, который запускает метод Users.all (), перемещается по всей цепочке зависимостей от класса Users к классу Http, к axios, к API и обратно.

Это легко заметить, если, например, вы выключите свой Wi-Fi и снова запустите тесты; на этот раз они потерпят неудачу, выбросив неприятную сетевую ошибку от axios (поверьте мне, я пробовал.)

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

Выпустите насмешки!

Итак, давайте смоделируем класс Http, а затем используем макет для нашего теста класса User вместо реального класса. К счастью, Jest делает это довольно легко, но есть пара подводных камней, о которых мы поговорим позже.

Подделанный класс Http выглядит так:

Первые две ошибки: макет должен: 1) иметь то же имя файла, что и макет класса; и 2) находиться внутри папки с именем __mocks__ внутри папки, в которой находится фиктивный класс.

Вероятно, есть способы изменить это поведение Jest по умолчанию, но наблюдение за этой первой ошибкой избавит вас от множества головных болей (мы уже почесали голову за вас!)

Нам нужно сделать еще один последний шаг. Нам нужно указать Jest использовать имитацию класса Http при запуске теста для класса Users. Для этого нам просто нужно добавить следующую строку в файл users.spec.ts сразу после операторов импорта и перед первым блоком описания:

jest.mock(‘./common/http’);

Если мы снова запустим тесты сейчас с выключенным Wi-Fi, они все равно пройдут. Итак, теперь мы можем утверждать, что тесты эффективно изолированы. Мы такие классные!

Когда дела идут на юг

Жизнь прекрасна, когда конечные точки API или запросы к базе данных отвечают должным образом и все такое, но давайте посмотрим правде в глаза: даже лучший API или самая устойчивая БД иногда рушится.

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

Итак, мы должны быть готовы к тому, что дела идут на юг. И наши модульные тесты также должны учитывать ошибки, идущие на юг.

Давайте изменим наш файл спецификаций, чтобы охватить гипотетический случай ошибки.

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

  1. [строка 2] Импорт зависимости, которую нужно изменить. Поскольку мы говорим Jest заменить настоящий класс на фиктивный в строке 5, мы собираемся фактически модифицировать фиктивный класс.
  2. [строки 21–28] Создание нового теста для покрытия случая ошибки.
  3. [строки 22–24] Изменение прототипа класса Http для изменения метода get () так, чтобы он возвращал ошибку вместо массива.
  4. [строки 26–27] Проверка того, что результат проверенного метода теперь является действительной ошибкой.

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

🛳 it!

Это все. Надеюсь, это было полезно. Теперь ты можешь продолжать быть крутым.

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

Спасибо за прочтение!