Тестовые двойники и тестовые данные

Что это такое

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

Почему это важно

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

Ключевые идеи

Выбирайте двойник по тому, что тестируете

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

const clock = { now: () => new Date("2026-01-01T00:00:00Z") };   // заглушка
const repo = new InMemoryOrders();                              // подделка

Двойники на границе, которой владеете

Определите интерфейс, который вы контролируете, и подменяйте его, а не мокайте стороннюю библиотеку напрямую. Такой интерфейс стабилен, мал и выражает то, что нужно вашему коду. А API библиотеки велик, будет меняться по собственному графику и протаскивает эту библиотеку в каждый тест. Это та же инверсия зависимостей, которая и делает продакшен-код тестируемым.

interface Clock { now(): Date }
// модуль принимает Clock; тесты передают фиксированный, продакшен — настоящий

Проверяйте меньше вызовов, утверждайте больше результатов

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

Стройте данные маленькими умолчаниями плюс переопределениями

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

const order = makeOrder({ total: 2500, currency: "GBP" });

Держите один канонический пример на правило

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

Практика

Замените мок подделкой

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

Готово, когда: тест больше не упоминает ни одного имени метода хранилища и по-прежнему проваливается, если поведение модуля сломано.

Сожмите построитель данных

Найдите самую длинную подготовку данных в своём наборе. Замените её короткой фабрикой плюс переопределениями на каждый тест.

Готово, когда: подготовка каждого теста показывает только поля, важные для этого случая, и весь файл стал короче.

Частые ошибки

  • Мокание каждого помощника — тест превращается в спецификацию текущей реализации и блокирует рефакторинг; исправить: заглушайте только то, что нужно модулю, и утверждайте об исходах.
  • Мокание сторонней библиотеки напрямую — тест ломается по графику библиотеки и протаскивает её типы повсюду; исправить: зависьте от интерфейса, которым владеете, а библиотеку адаптируйте на периферии.
  • Фабрика с ветвлениями и опциями — вспомогательный код теста понять труднее, чем сам тест; исправить: держите умолчания правдоподобными, а фабрику короткой.
  • Копирование большой фикстуры в несколько тестов — изменение схемы требует найти каждую копию; исправить: определите один канонический пример и ссылайтесь на него.
  • Утверждение о порядке внутренних вызовов — безобидная перестановка ломает сборку; исправить: утверждайте только о взаимодействии, которое является требованием, а предпочитайте исходы.

Что читать дальше

  • Что делает модульный тест хорошим (01-cs-fundamentals/10-unit-testing/01-what-makes-a-good-unit-test) — правило «поведение важнее реализации», которому служат эти двойники, поэтому прочитайте его первым.
  • Интеграционное тестирование (01-cs-fundamentals/21-integration-testing) — что скрывает подделка и когда вместо неё нужен настоящий помощник.
  • Принципы SOLID (01-cs-fundamentals/09-solid-principles) — проектное давление, которое делает помощников заменяемыми без церемоний.