Тестовые двойники и тестовые данные
Что это такое
Тестовый двойник — это подставной объект, который встаёт на место помощника, иначе вызванного модулем. Двойники бывают разными: заглушки возвращают заранее заданные ответы, подделки представляют собой небольшие реализации, которые действительно работают, шпионы записывают, как их вызвали, а моки утверждают ожидания о вызовах. Тестовые данные — это вход, который вы подаёте модулю; обычно их строит маленькая фабрика с разумными умолчаниями, чтобы каждый тест называл только то, что важно именно ему.
Почему это важно
Модульные тесты остаются быстрыми и сфокусированными только тогда, когда помощники модуля заменены, и выбор двойника решает, насколько тест связан с реализацией. Мок, который утверждает последовательность вызовов, превращает тест в описание текущего кода — а это противоположность тому, что нужно. У подготовки данных та же проблема, только на более низком уровне: замысловатые построители делают тесты нечитаемыми, а встроенные литералы, повторённые повсюду, — шумными. Привычки из этой записи и держат набор честным и быстрым по мере роста.
Ключевые идеи
Выбирайте двойник по тому, что тестируете
Используйте заглушку, когда модулю нужен ответ и вам неважно, как его запросили. Используйте подделку, когда у помощника есть настоящее поведение, от которого модуль зависит, — например, хранилище, которое сохраняет и возвращает значения. Используйте шпиона скупо, когда взаимодействие и есть то, что тестируется по-настоящему: отправка письма, публикация события. «Заглушить всё» даёт тесты, которые ломаются при любой перестройке модуля.
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) — проектное давление, которое делает помощников заменяемыми без церемоний.