Эвристики качества теста и антипаттерны
Сцена · тест, который никогда не краснеет
Спроси себя: видел ли ты этот тест красным хоть раз. Если нет, ты не знаешь, работает ли он. Тест, который не падал ни на одном баге, ничем не отличается от закомментированного: он создаёт ощущение защиты, не давая защиты. Пожарная сигнализация, которая ни разу не сработала, может быть исправной, а может быть с вынутой батарейкой. Узнать можно только проверкой.
В этом уроке несколько простых эвристик, которыми можно оценить тест, не запуская никаких инструментов. И галерея антипаттернов: как выглядит тест, который притворяется тестом.
Карта урока · что заберёшь
- получишь набор быстрых эвристик качества теста для код-ревью;
- научишься узнавать вечнозелёный тест, тавтологию и тест, дублирующий реализацию;
- освоишь эвристику “сломай код” как ручную версию будущего инструмента;
- сверишь свои тесты с четырьмя столпами хорошего набора.
Раздел 1 · эвристики без инструментов
Идея словами
Четыре вопроса, которые можно задать любому тесту прямо на ревью:
- Упадёт ли тест, если сломать код? Если никакая правка реализации его не роняет, тест пустой.
- Читается ли он как спецификация? Имя и тело должны объяснять поведение, а не описывать механику.
- Один ли у него повод падать? Если тест проверяет пять вещей сразу, он будет краснеть по непонятной причине и его будут игнорировать.
- Не дублирует ли он продакшн-логику в assert? Если в ассерте повторяется та же формула, что в коде, тест проверяет сам себя, а не код.
Шаг · прогон эвристик
Вот два теста одной функции. Прогони по ним четыре вопроса.
// A
it('работает', () => {
const r = applyPayment(makeAccount({ balance: 100 }), makePayment(30));
expect(r).toBeDefined();
});
// B
it('списывает сумму с баланса', () => {
const r = applyPayment(makeAccount({ balance: 100 }), makePayment(30));
expect(r.balance).toBe(70);
});
Тест A не упадёт почти ни на каком баге: r определён, пока функция вообще что-то возвращает. Имя “работает” не спецификация. Тест B падает, если баланс посчитан неверно, читается как строка контракта, проверяет одно. A это декорация, B это тест.
Что взять с собой
- хороший тест падает на сломанном коде, плохой нет;
- имя теста это спецификация, не “работает”;
- один тест, один повод падать.
Раздел 2 · галерея антипаттернов
Вечнозелёный тест
Тест, который зелёный при любой реализации. Крайний случай:
it('проверяет сложение', () => {
expect(true).toBe(true);
});
Очевидный пример смешон, но в жизни он маскируется: слишком слабый assert (toBeDefined, toBeTruthy на структуре), проверка несущественного поля, мок, который всегда возвращает ожидаемое.
Тавтология
Тест, в ассерте которого повторяется логика самой функции:
it('суммирует', () => {
const a = 2;
const b = 3;
expect(sum(a, b)).toBe(a + b);
});
Здесь a + b в ассерте это та же операция, что внутри sum. Если в sum баг (например, умножение вместо сложения), тест с тем же выражением в ассерте не поможет: ты сравниваешь функцию с её же предполагаемой формулой, а не с известным правильным ответом. Правильно зафиксировать конкретный ожидаемый результат: expect(sum(2, 3)).toBe(5).
Мок, проверяющий мок
Тест, где замокано всё, что функция трогает, и ассерт проверяет, что мок был вызван так, как настроен. Такой тест проверяет конфигурацию мока, а не поведение кода. Полную классификацию дублей и что делать, когда моков слишком много, разберём в блоке про unit-тесты.
Что взять с собой
- вечнозелёный тест зелёный при любой реализации;
- тавтология повторяет формулу кода в ассерте вместо известного ответа;
- если ассерт проверяет только мок, кода в проверке нет.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.