Что тестировать: поведение, а не реализацию
Сцена · не считай винтики, проверь, что машина едет
Приёмщик на заводе не пересчитывает винтики в моторе. Он заводит машину и смотрит, едет ли она, тормозит ли, горят ли фары. Его не волнует, сколько витков в катушке: это деталь сборки, она может меняться от партии к партии. Его волнует поведение машины на дороге.
Тесты бывают двух сортов. Одни проверяют, что машина едет (поведение через публичный интерфейс). Другие считают винтики (детали реализации: приватные методы, внутренние вызовы, имена полей). Первые переживают рефакторинг, вторые краснеют от любой пересборки, даже когда машина едет ровно. Этот урок про то, как писать первые и не плодить вторые.
Карта урока · что заберёшь
- поймёшь, почему тест на реализацию краснеет зря и становится обузой;
- увидишь это на одном безопасном рефакторе своими глазами;
- научишься расставлять тесты по ROI: что покрывать в первую очередь, что почти никогда;
- разберёшь, где живут баги, которых happy-path не ловит, это edge cases.
Раздел 1 · поведение против реализации
Идея словами
Поведение это то, что видно снаружи. Реализация это как оно устроено внутри. Тест должен проверять первое и игнорировать второе.
Причина простая: реализация меняется чаще, чем контракт. Ты переписал цикл на reduce, вынес помощник, переименовал поле. Поведение то же, но тест, который проверял “вызвался ли приватный addLine”, покраснел. Ты потратил время на починку теста, который ничего не нашёл. Это не страховка, это лишний вес.
Картинка · один рефактор, два теста
Переключи рефактор с цикла на reduce. Тест на поведение (cartTotal([10, 20, 30]) равно 60) остаётся зелёным: сумма та же. Тест на реализацию (ждёт три вызова addLine) краснеет: помощника больше нет. Касса считает правильно, а тест врёт, будто что-то сломалось. Вот цена привязки к реализации.
Шаг 1 · красный по ошибке
Допустим, у нас есть такой тест на реализацию:
it('вызывает addLine на каждую позицию', () => {
const spy = vi.spyOn(internals, 'addLine');
cartTotal([10, 20, 30]);
expect(spy).toHaveBeenCalledTimes(3);
});
Он проверяет не результат, а маршрут вычисления. После рефактора на reduce маршрут другой, тест красный, хотя сумма верна.
Шаг 2 · зелёный по поведению
it('складывает цены позиций в итог', () => {
expect(cartTotal([10, 20, 30])).toBe(60);
});
Этот тест не знает и не хочет знать, как именно складываются цены. Он переживёт цикл, reduce, рекурсию, любую переборку внутренностей. Он краснеет только если сумма стала неверной, то есть когда сломалось то, что важно.
Когда код пишет агент
Это главная причина тестировать поведение в эпоху агентов. Агент перепишет реализацию при первой же просьбе “оптимизируй” или “почисти”. Тест на приватный метод привязывает тебя к тому, как агент написал код сегодня. Завтра он напишет иначе, и твой набор покраснеет на ровном месте. Поведенческий тест переживает переписывание, потому что фиксирует контракт, а не сиюминутную форму кода.
Что взять с собой
- проверяй результат через публичный интерфейс, не маршрут вычисления;
- тест на реализацию краснеет от рефактора, который ничего не сломал;
- поведенческий тест переживает переписывание кода, в том числе агентом.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.