Внедрение тестов в проект без тестов: сначала зафиксируй, потом меняй
Сцена · сначала зафиксируй, потом меняй
Тебе достался проект без единого теста. Не учебный, а реальный: касса работает, деньги ходят, но как именно работает вон тот модуль выгрузки в бухгалтерию, не знает уже никто. Менеджер просит поправить формат отчёта. Открываешь файл, а там зашитый 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.