Лучшие практики модульных тестов TypeScript, часть 4: чистая структура наборов тестов

Предложение чистой структуры для организации юнит-тестов

Псс!

Вы можете найти часть 1 здесь: Введение,
и часть 2 здесь: IDE и настройка проекта,
и часть 3 здесь: определения и правила,
И часть 5 здесь: как модульное тестирование (почти) всего на TypeScript!

И снова здравствуйте! Здесь мы предлагаем простое руководство по легкой организации тестов!

Код установки

Как вы могли видеть в части 2, мы настроили тестовый сценарий так, чтобы сначала запустить файл с именем setup.spec.ts. Вот пример действительного и простого кода установки:

Что мы здесь делаем?

  • Мы инициализируем библиотеку sinon-chai, которая поможет нам сделать несколько простых утверждений;
  • Мы настраиваем sinon для восстановления состояния двойных тестов после каждого теста. Это очень важно, чтобы один тест не мешал другому;

Что еще мы можем сделать в этом файле?

  • Мы могли бы инициализировать другие библиотеки chai;
  • Мы могли бы установить другое глобальное поведение для модульных тестов;
  • Мы могли бы экспортировать некоторые общие пользовательские тестовые функции, чтобы помочь нам в других тестах, например, ряд общих утверждений;

Что мы не должны делать в этом файле?

  • Мы не должны инициализировать внешний экземпляр службы, такой как база данных или служба кеша. Модульные тесты не должны иметь доступ к такому сервису!
  • Мы не должны устанавливать объекты, которые будут использоваться в других тестовых файлах. Для простоты и во избежание вторжения в область применения каждый тест должен создавать минимально необходимые объекты для каждого теста;
  • Мы не должны заглушать ни один метод! Оставьте заглушки для каждого набора тестов!

Правила исходного кода для простой тестовой структуры

Хороший модульный тест начинается с хорошего и тестируемого исходного кода. Итак, для достижения однородной структуры теста мы рекомендуем вам следовать этим правилам для каждого файла исходного кода:

  • Каждый файл должен экспортировать только один объект. Это может быть константная структура, функция или класс. Если вы хотите иметь файл, экспортирующий несколько объектов, создайте папку, файл для каждого экспорта и индексный файл для этого;
  • Он не должен был иметь не раскрытых внутренних функций. Что-то подобное делает модульный тест для таких функций невозможным, требуя от вас косвенного тестирования. Переместите их в другой файл или создайте константную структуру, содержащую каждую экспортированную функцию;
  • У вас не должно быть исходного кода с функциями/классами и непосредственно исполняемым кодом, то есть кодом, который будет запускаться вместе с импортом такого файла. Тестировать такие сценарии можно, но это сложнее и уродливее. Собственно, единственный файл с непосредственно исполняемым кодом, который должен быть у вас в проекте (например, экспресс-API), — это src/index.ts, основной. Хороший пример основного кода выглядит следующим образом:
import { start } from './server.ts';
start();

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

Объяснение структуры модульного теста

Здесь мы предлагаем однородную организацию для каждого модульного теста со следующими правилами:

  • Файл должен находиться в папке test в каталоге, отражающем исходную структуру src. Кроме того, имя файла должно быть theNameOfTheTestedFile.spec.ts. Таким образом, у вас будет модульный тест для каждого файла, содержащего тестируемые строки вашего проекта;
  • У вас должен быть модульный тест для основного файла. Это позволит учитывать все исходные файлы вашего проекта при измерении охвата, так как все файлы прямо или косвенно загружаются основным;
  • Первым describe вашего кода должно быть имя единственного экспортируемого объекта;
  • Если экспортируемый объект является функцией, создайте beforeEach, чтобы установить все двойники, которые будут использоваться в ваших тестовых примерах;
  • Если экспортируемый объект является классом, создайте beforeEach, чтобы установить экземпляр целевого объекта и внедренных служб как пустой объект;
  • Пусть в соответствующем разделе it будут заданы конкретные сценарии тестирования;
  • Для тестов классов для каждого метода создайте beforeEach, чтобы установить все двойники, которые будут использоваться в ваших тестовых примерах;
  • Для каждой созданной заглушки необходимо иметь некоторое утверждение. Это потому, что заглушки без утверждений могут скрывать неожиданное поведение;
  • Утверждения должны быть настолько ограничительными, насколько это необходимо;

Следуя этим правилам, вы можете протестировать класс следующим образом:

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

Но давайте обсудим немного глубже некоторые детали кода выше:

MyService высмеивается как пустой объект? Почему это?

На это есть веские причины!

  • Вы вообще не получаете доступ к MyService, что предотвращает вторжение в область в вашем тесте;
  • Любая строка MyClass, использующая MyService, которая вызывает метод, который вы не заглушили, выдаст ошибку, которая поможет вам никого не забыть;
  • поскольку служба создается в beforeEach, нет риска, что один тест повлияет на другой из-за какой-либо ошибки настройки;
  • Каждый тестовый пример будет иметь только необходимый код заглушки для запуска. Ни больше ни меньше. Это отлично подходит для долгосрочного обслуживания, так как помогает тесту сломаться, когда он должен. Например, что, если тестируемый метод начнет вызывать другой метод MyService? Вы не согласны с тем, что тест нужно пересмотреть? Ну, так как для такого метода не создавалась заглушка, вам придется;

Почему я должен подтверждать каждую заявленную заглушку?

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

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

Как тестировать файлы без функций, классов или константных структур?

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

Посмотрите, этого достаточно, чтобы гарантировать работу этого блока. Ведь за что в таком случае отвечает наш основной файл? Просто вызвать server.start() и все!

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

Что, если я создаю библиотеку, а мой основной файл выполняет только экспорт?

В этом случае вы можете просто сослаться на свой основной файл в файле setup.spec.ts, выполнив следующие действия:

import '../src/index';

Этого будет достаточно, чтобы гарантировать обнаружение всех линий, но будьте осторожны! Не делает этого, если есть исполняемый код вне функций или классов, так как этот код может быть выполнен и может повлиять на ваши тесты! Желательно следовать нашим советам и избегать такого кода, оставляя таким только основной :)

Надеюсь, вам понравилось и до следующей статьи!