Раздел 29 · Тестирование

Дубли: классификация, овермок и in-memory fakes

middle-senior~45 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Дубли: классификация, овермок и 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.