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

Внедрение тестов в проект без тестов: сначала зафиксируй, потом меняй

middle~40 мин

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

Внедрение тестов в проект без тестов: сначала зафиксируй, потом меняй

Сцена · сначала зафиксируй, потом меняй

Тебе достался проект без единого теста. Не учебный, а реальный: касса работает, деньги ходят, но как именно работает вон тот модуль выгрузки в бухгалтерию, не знает уже никто. Менеджер просит поправить формат отчёта. Открываешь файл, а там зашитый Date.now(), fetch посреди логики, мутируемое модульное состояние и ни одного шва, чтобы подлезть. Поправишь, и неясно, что сломаешь.

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

Карта урока · что заберёшь

  • поймёшь, чем характеризующий тест отличается от обычного и зачем он;
  • напишешь характеризующий тест на реальный legacy-модуль spool.ts;
  • научишься выстраивать приоритеты внедрения по карте критических путей;
  • освоишь правило бойскаута: наращивать покрытие по ходу работы, без отдельного спринта.

Раздел 1 · характеризующий тест

Идея словами

Характеризующий тест не судит, правильно ли поведение, он его фотографирует. Обычный тест говорит “функция должна возвращать 70”. Характеризующий говорит “функция сейчас возвращает вот это, и я фиксирую это как факт, даже если оно странное”. Разница принципиальная: ты не разбираешься, как код устроен, ты строишь страховочную сетку, чтобы поймать момент, когда поведение поедет.

Это прямое продолжение урока 19 про непроверяемый код. Там мы увидели, почему spool.ts нельзя нормально протестировать: зашитые зависимости, нет швов. Здесь делаем первый шаг к спасению: фиксируем поведение через подмену глобалей, даже грязно, чтобы потом рефакторить безопасно.

Шаг 1 · фотографируем поведение legacy

spool.ts зашил Date.now() и fetch прямо в логику. Чтобы хоть как-то зафиксировать поведение, приходится подменять их глобально, и это уже сигнал о плохих швах. Записываем фактический вывод в assert:

// apps/kassa-api/test/spool-characterization.test.ts
import { afterEach, describe, expect, it, vi } from 'vitest';

import { spoolPayments } from '~/legacy/spool.ts';

const realFetch = globalThis.fetch;

describe('spool (характеризующее поведение legacy)', () => {
  afterEach(() => {
    globalThis.fetch = realFetch;
    vi.restoreAllMocks();
  });

  it('фиксирует сумму, счётчик и отметку времени на замороженных часах', async () => {
    vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);
    const sent: string[] = [];
    globalThis.fetch = (async (_url: string, init?: RequestInit) => {
      sent.push(String(init?.body));
      return new Response(null, { status: 200 });
    }) as typeof fetch;

    const result = await spoolPayments([
      { account: 'acc-1', amount: 30 },
      { account: 'acc-2', amount: 70 },
      { account: 'acc-bad', amount: -5 },
    ]);

    expect(result).toEqual({ total: 100, count: 2, sentAt: 1_700_000_000_000 });
    expect(sent[0]).toContain('TOTAL\t100');
  });
});

Обрати внимание: мы не спрашиваем, правильно ли spool пропускает отрицательную сумму через console.log посреди логики. Мы фиксируем, что он её пропускает и считает count: 2. Если завтра кто-то изменит это поведение, тест покраснеет, и мы заметим. Это и есть страховка.

Шаг 2 · даже странное поведение это факт

Второй характеризующий тест фиксирует, что spool бросает на не-ok ответе сети:

// apps/kassa-api/test/spool-characterization.test.ts
it('бросает, когда внешняя система ответила не ok', async () => {
  vi.spyOn(Date, 'now').mockReturnValue(0);
  globalThis.fetch = (async () => new Response(null, { status: 500 })) as typeof fetch;

  await expect(spoolPayments([{ account: 'a', amount: 1 }])).rejects.toThrow('spool failed: 500');
});

Мы не утверждаем, что бросать исключение строкой это хороший дизайн (это антипаттерн, ошибка должна быть значением). Мы фиксируем, что сейчас он бросает именно так. После рефакторинга под Effect (урок 19) поведение изменится осознанно, и характеризующий тест нам про это скажет.

Когда код пишет агент

Агент охотно генерирует такой spool: зашитые зависимости короче писать, и на первый взгляд всё работает. Характеризующий тест это твой способ взять под контроль чужой (в том числе агентский) код, который ты не писал и не понимаешь до конца. Сначала фотографируешь, потом просишь агента рефакторить, и фотография ловит, если он изменил поведение, а не только форму.

Что взять с собой

  • характеризующий тест фиксирует текущее поведение, не оценивая его правильность;
  • даже странное и грязное поведение это факт, который защищает от незаметной регрессии;
  • глобальная подмена Date.now и fetch в характеризующем тесте это сигнал о плохих швах.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.