Наверняка многие слышали старую шутку:
Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте.
Однако мало кто уделяет столько же внимания качеству тестов. А зря: тесты — это тоже код, причём разработчик обычно начинает внимательно читать их в самый неприятный момент — когда что-то упало и нужно быстро понять причину.
Поэтому старую шутку я бы переформулировал так:
Пишите тесты так, чтобы вам был искренне благодарен тот человек, который однажды будет их чинить.
Этим человеком вполне можете оказаться вы сами. Я бывал в такой ситуации и был себе благодарен за то, что когда-то не поленился написать тесты нормально.
Тест как описание поведения
Документация описывает поведение, особенности и ограничения. Тесты проверяют поведение, особенности и ограничения. Их задачи заметно пересекаются.
Хорошо написанная документация помогает быстро разобраться, как работает система. Хорошо написанные тесты делают то же самое, только ещё и проверяют, что описание не разошлось с кодом.
Разные типы тестов документируют систему на разном уровне:
- юнит-тесты — поведение небольшого компонента;
- интеграционные — поведение сервиса и его связей;
- end-to-end — поведение всей системы;
- нагрузочные — пределы системы под нагрузкой.
У каждого уровня своя аудитория. Юниты чаще нужны разработчикам. Интеграционные и end-to-end тесты помогают ещё тестировщикам, аналитикам и продактам. Нагрузочные тесты дают контекст разработчикам, тестировщикам и SRE.
Конечно, тесты не отменяют документацию. Но если команда относится к ним как к исполняемому описанию поведения, становится заметнее и качество требований: если разработчик не понимает, как проверить задачу, то, скорее всего, он ещё не до конца понял, что нужно реализовать.
Полнота и краткость
Хороший тест должен быть одновременно полным и кратким.
Полный тест содержит всю информацию, необходимую для понимания сценария. Читатель должен разобраться, как тест работает и за счёт чего код выдаёт нужный результат. Сюда относятся создание заглушек, подмена ответов, явное описание ожидаемого результата и даже нормальные названия переменных.
Полный тест понятен без дополнительного ковыряния в документации и исходном коде.
Краткий тест не содержит лишней информации. Обычно визуальный мусор появляется при создании моков: пятнадцать строк подготовки при общей длине теста в двадцать. Такие детали можно спрятать в генераторе мок-объектов и оставить в сценарии только значимые параметры.
Краткость усиливает полноту, потому что читателю нужно проанализировать меньше информации. Вместе эти характеристики дают тест, в котором есть всё нужное и нет ничего лишнего.
Название должно описывать поведение
Название теста — ключ к его пониманию. Хорошее название сразу объясняет, какой сценарий проверяется. На этой мысли построен behavior-driven development.
Для названий я использую несколько правил.
- Писать полными предложениями. Мы не газету верстаем и буквы экономить не нужно.
- Указывать условия и последствия. Из названия должно быть понятно, что именно произойдёт и при каких обстоятельствах.
- Следовать принципу DAMP. В книге «Делай как в Google» он расшифровывается как descriptive and meaningful phrases — описательные и осмысленные фразы.
Как не надо:
Проверка валидации;Должен вернуть ошибку, если всё плохо;Проверяем правильное поведение.
Как лучше:
Должен выбросить UserNotFoundException, если пользователь не найден по ID;Должен вернуть false, если пользователь младше 18 лет;Должен зарегистрировать пользователя, если указанного email нет в базе.
Огромную неструктурированную документацию никто не читает. С запутанными тестами происходит то же самое. Хороший тест читается как короткое и актуальное описание поведения системы — именно поэтому однажды он может сэкономить кому-то часы разбирательств.
Тестировать поведение, а не внутренности
При написании теста я стараюсь смотреть на компонент как на чёрный ящик. Внутреннее устройство либо слишком сложное, либо просто не имеет значения для проверяемого сценария. Подали конкретный вход — получили ожидаемый выход.
Тесты, привязанные к внутренним циклам, условиям и приватным методам, ломаются при рефакторинге, даже если поведение компонента осталось прежним. В итоге они защищают текущую реализацию, а не результат.
Особенно подозрительно выглядит необходимость добираться в тесте до приватного метода. Если важное поведение нельзя проверить через публичный контракт, возможно, проблема находится не в тесте, а в границах самого компонента.
Сколько тестов достаточно
На этот вопрос часто отвечают процентом покрытия. Coverage полезен как стартовая метрика, особенно если тестов в проекте почти нет. Но он показывает лишь то, какие строки выполнялись во время тестов, а не качество проверенных сценариев.
Если поставить слишком высокую планку, команда начинает писать тесты ради покрытия. Появляются проверки деталей реализации, растёт время прогонов, а уверенности в бизнес-сценариях больше не становится.
Для себя я использую несколько эмпирических правил:
- Процент покрытия нужен как ограничитель, но сам по себе не является целью.
- Покрытие не должно незаметно снижаться без осознанного решения команды.
- Каждый тест должен защищать понятное поведение или важный риск.
- Если тест можно удалить без потери уверенности и описанных сценариев, скорее всего, он лишний.
Универсального числа здесь нет. Чем опытнее команда и дороже ошибка, тем содержательнее должен быть разговор о рисках вместо механического требования «сделать 100%».