Заметка

Как писать хорошие тесты

Наверняка многие слышали старую шутку:

Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте.

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

Поэтому старую шутку я бы переформулировал так:

Пишите тесты так, чтобы вам был искренне благодарен тот человек, который однажды будет их чинить.

Этим человеком вполне можете оказаться вы сами. Я бывал в такой ситуации и был себе благодарен за то, что когда-то не поленился написать тесты нормально.

Тест как описание поведения

Документация описывает поведение, особенности и ограничения. Тесты проверяют поведение, особенности и ограничения. Их задачи заметно пересекаются.

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

Разные типы тестов документируют систему на разном уровне:

  • юнит-тесты — поведение небольшого компонента;
  • интеграционные — поведение сервиса и его связей;
  • end-to-end — поведение всей системы;
  • нагрузочные — пределы системы под нагрузкой.

У каждого уровня своя аудитория. Юниты чаще нужны разработчикам. Интеграционные и end-to-end тесты помогают ещё тестировщикам, аналитикам и продактам. Нагрузочные тесты дают контекст разработчикам, тестировщикам и SRE.

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

Полнота и краткость

Хороший тест должен быть одновременно полным и кратким.

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

Полный тест понятен без дополнительного ковыряния в документации и исходном коде.

Краткий тест не содержит лишней информации. Обычно визуальный мусор появляется при создании моков: пятнадцать строк подготовки при общей длине теста в двадцать. Такие детали можно спрятать в генераторе мок-объектов и оставить в сценарии только значимые параметры.

Краткость усиливает полноту, потому что читателю нужно проанализировать меньше информации. Вместе эти характеристики дают тест, в котором есть всё нужное и нет ничего лишнего.

Название должно описывать поведение

Название теста — ключ к его пониманию. Хорошее название сразу объясняет, какой сценарий проверяется. На этой мысли построен behavior-driven development.

Для названий я использую несколько правил.

  1. Писать полными предложениями. Мы не газету верстаем и буквы экономить не нужно.
  2. Указывать условия и последствия. Из названия должно быть понятно, что именно произойдёт и при каких обстоятельствах.
  3. Следовать принципу DAMP. В книге «Делай как в Google» он расшифровывается как descriptive and meaningful phrases — описательные и осмысленные фразы.

Как не надо:

  • Проверка валидации;
  • Должен вернуть ошибку, если всё плохо;
  • Проверяем правильное поведение.

Как лучше:

  • Должен выбросить UserNotFoundException, если пользователь не найден по ID;
  • Должен вернуть false, если пользователь младше 18 лет;
  • Должен зарегистрировать пользователя, если указанного email нет в базе.

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

Тестировать поведение, а не внутренности

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

Тесты, привязанные к внутренним циклам, условиям и приватным методам, ломаются при рефакторинге, даже если поведение компонента осталось прежним. В итоге они защищают текущую реализацию, а не результат.

Особенно подозрительно выглядит необходимость добираться в тесте до приватного метода. Если важное поведение нельзя проверить через публичный контракт, возможно, проблема находится не в тесте, а в границах самого компонента.

Сколько тестов достаточно

На этот вопрос часто отвечают процентом покрытия. Coverage полезен как стартовая метрика, особенно если тестов в проекте почти нет. Но он показывает лишь то, какие строки выполнялись во время тестов, а не качество проверенных сценариев.

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

Для себя я использую несколько эмпирических правил:

  1. Процент покрытия нужен как ограничитель, но сам по себе не является целью.
  2. Покрытие не должно незаметно снижаться без осознанного решения команды.
  3. Каждый тест должен защищать понятное поведение или важный риск.
  4. Если тест можно удалить без потери уверенности и описанных сценариев, скорее всего, он лишний.

Универсального числа здесь нет. Чем опытнее команда и дороже ошибка, тем содержательнее должен быть разговор о рисках вместо механического требования «сделать 100%».