Всем привет!
Каким должен быть хороший тест?
Я в первую очередь про модульные (unit), но в принципе правила применимы к любым.
Основные моменты:
1) правило Arrange, Act, Assert https://xp123.com/articles/3a-arrange-act-assert/
Тест делится на три части: подготовка тестовых данных, вызов тестового метода и проверка. Часто забывают про последнюю.
Тест должен что-то проверить: выброшенное исключение, сколько и каких было вызвано методов, состояние объекта.
Тест проверяющий только тот факт, что вызов прошел без исключения, добавляет покрытия, но по сути является недотестом. Его успех показывает, что что-то там выполнилось) Выполнено ли то, что должен делать метод - не ясно. Если API не позволяет проверить результат выполнения кода - это плохое API. Если же код legacy и рефакторить его сложно - можно воспользоваться Mockito.spy или рефлексией.
2) тестовый код - такой же код, как и боевой. К нему должны быть применены все практики написания чистого кода - https://www.litres.ru/robert-s-martin/chistyy-kod-sozdanie-analiz-i-refaktoring-6444478
Я вижу одно исключение - в тестовом коде норм использовать System.out.println
3) тест должен проверять одну операцию с одним или несколькими связанными наборами входных параметров, это удобно делать через параметризацию. При этом допустимо в тесте использовать несколько Assert. Хотя можно их вынести в один assert метод. Или использовать Soft Assert http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html#soft-assertions. Как по мне - все три варианта норм, дело вкуса.
4) все внешние объекты, требуемые тестируемому методу, должны быть заглушены. Наиболее удобно использовать Mockito.mock или Mockito.spy, но если надо - можно наследоваться от интерфейса и сделать тестовый двойник самому. Если глушить приходится слишком много - повод задуматься про архитектуру кода
5) если стандартного API Mockito или самописных заглушек не хватает, и руки тянутся к включить "секретные" опции Mockito для того, чтобы заглушить private, static или наследоваться от final класса - тоже повод задуматься про архитектуру. Хотя как быстрое решение для legacy допустимо.
6) в поставке для ПРОМа не должно быть тестового кода, боевой код не должен использовать тестовые зависимости и Helpers. В боевом коде не должно быть "ловушек" для успешного выполнения тестов - т.е. выражений типа
if (isTestRun()) {
7) тесты - это документация к коду. Для этого тест должен легко читаться. А чтобы этого добиться - нужно выносить все лишние во вспомогательные методы для Arrange и Assert, передавая как параметры в эти методы только то, что непосредственно влияет на конкретный тест. В этом плане при выборе между @ BeforeEach методом и обычным нужно выбирать обычный - так замысел теста легче читается.
А при возникновении вопроса чтобы бы добавить в тесты, или может уже хватит, стоит в первую очередь думать о закрытии всех основных комбинаций входных параметров, а не о покрытии. Требуемый процент покрытия можно подкрутить, главное как и у тестировщиков - полнота тестовой модели. И читаемость тестов
8) тесты должны быть антихрупкими. Т.е. тест не должен падать при любых правках кода, а тем более - при правках кода, который на первый взгляд никак не связан с тестом. Задача теста - облегчить рефакторинг и доработки, а не усложнить их) Готовых рецептов тут нет, но если данные для assert меняются каждый релиз, значить проверять надо что-то еще. Пример: если речь про API - атрибуты JSON, а не полный JSON
9) тесты не должны зависеть от результата других тестов, от порядка выполнения и от среды
10) из логов запуска упавшего теста должно быть понятно, в чем ошибка. Для этого тест должен выполняться быстро, чтобы запускать его после каждого изменения, тестовые методы должны быть названы осознано, и самое сложное - в assert-ах должны быть сообщения об ошибках. И \ или использовать power asserts, которые подробно выводят в лог что пришло в Assert метод https://github.com/bnorm/kotlin-power-assert
P.S. Кроме JUnit и Mockito есть еще много интересных тестовых фреймворков, расскажу о них в других постах.
#unittests #cleancode