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

Эвристики качества теста и антипаттерны

middle~40 мин

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

Эвристики качества теста и антипаттерны

Сцена · тест, который никогда не краснеет

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

В этом уроке несколько простых эвристик, которыми можно оценить тест, не запуская никаких инструментов. И галерея антипаттернов: как выглядит тест, который притворяется тестом.

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

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

Раздел 1 · эвристики без инструментов

Идея словами

Четыре вопроса, которые можно задать любому тесту прямо на ревью:

  1. Упадёт ли тест, если сломать код? Если никакая правка реализации его не роняет, тест пустой.
  2. Читается ли он как спецификация? Имя и тело должны объяснять поведение, а не описывать механику.
  3. Один ли у него повод падать? Если тест проверяет пять вещей сразу, он будет краснеть по непонятной причине и его будут игнорировать.
  4. Не дублирует ли он продакшн-логику в 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.