Что делает модульный тест хорошим

Что это такое

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

Почему это важно

Тесты — код, который вы сопровождаете вечно, и плохо написанный тест хуже отсутствующего. Он проваливается по посторонним причинам, он пришпиливает деталь реализации, мешающую улучшению, и он приучает команду игнорировать красный. Однако разница между набором, которому доверяют, и набором, который пропускают, не в покрытии. Она в том, значит ли что-нибудь провал. Чтобы этого добиться, нужны несколько конкретных привычек, и их дёшево завести, пока набор маленький.

Ключевые идеи

Тестируйте поведение, а не реализацию

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

// хрупко: проверяет «как»
expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({ id: 7 }));

// устойчиво: проверяет «что»
expect(result).toEqual({ id: 7, status: "saved" });

Одна причина для провала

Тест, который утверждает пять вещей сразу, даёт одно сообщение о провале и прячет остальные четыре проблемы. Держите подготовку, единственное действие и утверждение в поле зрения, чтобы читатель мог понять случай без прокрутки. Когда тесту нужно несколько утверждений, спросите, не является ли он на самом деле несколькими случаями, разделяющими одну подготовку.

test("отвергает срок в прошлом", () => {
  expect(isValid(cardExpiredYesterday)).toBe(false);
});

Назовите ожидание

Имя теста документирует поведение модуля, если читать тесты списком. «возвращает цену со скидкой для участника» говорит вам, что делает система; «тест рассчитать цену 2» не говорит ничего при провальном запуске. Прочитайте имена в своём наборе как спецификацию, и вы найдёте и непокрытое поведение, и тесты, которые не утверждают ничего осмысленного.

calculatePrice
  - не применяет скидку гостю
  - применяет десять процентов участнику
  - округляет половины вверх до ближайшего пенни

Подготовка, действие, проверка

Дайте каждому тесту одну и ту же трёхчастную форму и держите части визуально раздельными: подготовка строит мир, действие выполняет одну операцию под тестом, проверка утверждает исход. Если их смешать, получится тест, который четыре раза вызывает модуль и проверяет после, — такой невозможно диагностировать, потому что не понять, какой вызов привёл к провалу.

const cart = cartWith(item("book", 1000));   // подготовка
const total = cart.total();                 // действие
expect(total).toBe(1000);                   // проверка

Предпочитайте настоящие значения умной подготовке

Тест должен читаться сверху вниз, без погони за вспомогательными функциями. Маленькие фабричные функции с разумными умолчаниями допустимы, а построитель на сто строк — нет. Когда подготовка разрастается, это обычно значит, что у модуля слишком много помощников, а это информация о продакшен-коде, а не о тесте.

Практика

Перепишите хрупкий тест

Найдите тест, который ломается всякий раз, когда вы рефакторите покрываемый им модуль. Перепишите его так, чтобы он утверждал только о наблюдаемом поведении, затем отрефакторите модуль и убедитесь, что тест по-прежнему проходит.

Готово, когда: вы изменили внутренности модуля, а тест не потребовал изменений.

Прочитайте свой набор как спецификацию

Откройте один файл тестов и прочитайте сверху вниз только имена тестов. Запишите, каким выглядит поведение модуля.

Готово, когда: вы видите хотя бы один пробел в спецификации и добавили для него тест.

Частые ошибки

  • Утверждения о внутренних вызовах — тест ломается при каждом рефакторинге и блокирует улучшения; исправить: утверждайте о возвращаемом значении или о состоянии, наблюдаемом вызывающим.
  • Несколько несвязанных утверждений в одном тесте — первый провал прячет остальные, и имя не описывает ни одного поведения; исправить: разделите на один тест на каждое ожидание.
  • Разделяемое изменяемое состояние между тестами — порядок тестов начинает иметь значение, и провалы зависят от того, какой тест выполнился первым; исправить: стройте свежее состояние в каждом тесте.
  • Тестирование фреймворка вместо кода — тест повторяет то, что библиотека уже гарантирует; исправить: тестируйте решения, которые принимает ваш код.
  • Именование тестов по методу — провальный запуск не даёт подсказки, какое поведение сломалось; исправить: назовите ожидание, которое показывает тест.

Что читать дальше