Дубли: классификация, овермок и in-memory fakes
Сцена · пять видов дублёров на съёмках
На съёмочной площадке вместо актёра в кадре может оказаться кто угодно. Манекен на дальнем плане, чтобы заполнить место. Дублёр, который произносит одну заученную реплику. Каскадёр, который делает трюк, а потом докладывает, как всё прошло. Все они подменяют настоящего актёра, но умеют разное, и путать их дорого.
В тестах ровно так же. Тестовый дубль это общее слово для подмены, а под ним пять разных штук. В уроке 06 мы закрепили три (stub, mock, fake), теперь развернём полную классификацию и, главное, разберём, почему чрезмерное мокирование превращает набор тестов в обузу.
Карта урока · что заберёшь
- разложишь дубли на пять видов и поймёшь, кто что умеет;
- увидишь, как овермок делает тест хрупким и привязанным к реализации;
- освоишь in-memory fake как лекарство от овермока;
- поймёшь, почему в Effect fake это просто тестовый
Layer.
Раздел 1 · полная классификация дублей
Идея словами
Пять видов по нарастанию умений:
- dummy: болванка, которая занимает место в сигнатуре, но в тесте не используется;
- stub: отдаёт заранее заданный ответ, ничего не запоминает;
- spy: как stub, но ещё запоминает, как его вызвали;
- mock: как spy, но ещё и сам проверяет, что его вызвали ожидаемым образом;
- fake: рабочая мини-реализация (например, репозиторий поверх
Mapвместо БД).
Ключевая разница проходит между mock и остальными. Stub, spy и fake отвечают на вопрос “что вернулось”. Mock отвечает на вопрос “как было вызвано”. Первое это проверка поведения, второе это проверка взаимодействия, и именно второе легко превращает тест в слепок реализации.
Картинка · карта дублей
Переключай вид дубля и смотри, что каждый делает на границе системы: stub только отдаёт ответ, spy ещё помнит вызовы, mock вдобавок проверяет их, fake это настоящая логика в миниатюре. Обрати внимание на правую колонку: в Effect-проекте место дубля чаще занимает тестовый Layer, а не отдельная мок-инфраструктура.
Шаг 1 · stub против mock на одном примере
Stub курса валют просто отдаёт число:
const ratesStub = { getRate: () => 75 }; // что вернулось
Mock того же курса ещё и настаивает на том, как его дёрнули:
expect(ratesSpy).toHaveBeenCalledWith('USD', 'RUB'); // как вызвано
Stub проверяет результат. Mock проверяет вызов. Если завтра ты поменяешь порядок аргументов или сольёшь два запроса в один, результат останется прежним, а mock покраснеет. Запомни эту асимметрию, на ней держится весь следующий раздел.
Что взять с собой
- пять видов: dummy, stub, spy, mock, fake;
- граница смысла проходит между mock и остальными;
- stub проверяет результат, mock проверяет вызов.
Шпион выглядит частью большого фреймворка, пока не напишешь его сам: внутри функция и массив в замыкании. Ниже редактор: собери свой аналог vi.fn с историей вызовов и структурным сравнением аргументов. Тесты гоняются прямо в браузере, ставить ничего не нужно.
Раздел 2 · овермок как болезнь
Идея словами
Овермок это когда замокано столько, что тест проверяет уже не поведение, а собственные заглушки. Такой тест краснеет от любого безобидного рефактора, потому что привязан к тому, как код был написан сегодня, а не к тому, что он делает.
Здесь живёт старый спор. Лондонская школа мокает всё подряд и проверяет взаимодействия. Классическая мокает только дорогой или недетерминированный край (сеть, время) и проверяет результат. В мире, где код переписывает агент, классическая выигрывает: поведенческий тест переживёт переписывание, а тест на взаимодействия покраснеет на ровном месте.
Шаг 1 · переусложнённый мок-тест
it('проводит платёж', () => {
const repo = {
getById: vi.fn().mockReturnValue(makeAccount({ balance: 100 })),
recordPayment: vi.fn(),
};
payAccount(repo, 'acc-1', 30);
// тест проверяет ВЫЗОВЫ, а не результат
expect(repo.getById).toHaveBeenCalledWith('acc-1');
expect(repo.recordPayment).toHaveBeenCalledTimes(1);
});
Что на самом деле проверено? Что код позвал getById и recordPayment. А баланс? А инвариант? Никто не смотрел. Перепиши payAccount так, чтобы он читал счёт один раз и кешировал, и тест покраснеет, хотя поведение не изменилось. Это слепок реализации, а не проверка поведения.
Шаг 2 · лечение fake-ом
Заменяем гору моков одной рабочей мини-реализацией репозитория и проверяем результат, а не вызовы:
it('списывает сумму и сохраняет новый баланс', async () => {
const repo = makeInMemoryRepo(); // fake поверх Map
await repo.create(money(100));
await payAccount(repo, accountId, money(30));
const account = await repo.getById(accountId);
expect(account.balance).toBe(70); // поведение, а не вызов
});
Теперь тест проверяет, что баланс действительно стал 70. Перепиши payAccount как угодно изнутри: пока поведение верное, тест зелёный. Это и есть классическая школа на практике.
Когда код пишет агент
Агент обожает мокать на каждый чих, потому что так проще “пройти тест”: подсунул заглушку, проверил, что её дёрнули, готово. Получаешь набор, который зелёный и при этом ничего не гарантирует, и краснеет от первого же рефактора. Это та самая декоративность, которую ловит mutation testing: мутируй логику внутри, мок-тест даже не заметит. Лекарство одно: fake вместо горы моков.
Что взять с собой
- овермок проверяет вызовы вместо поведения и краснеет от рефактора;
- классическая школа мокает только дорогой край, проверяет результат;
- лекарство от овермока это in-memory fake.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.