Как написать хороший юнит-тест

UnitTestAll-300x225Чтобы написать хороший юнит-тест, надо задать критерии его хорошести и придерживаться их. Это, вообще, относится к любой области деятельности: определяем цель, движемся к ней, если результат совпал с целью, значит всё удалось 🙂

Признаки хорошего юнит-теста

  • Тест должен проваливаться, когда поведение тестируемого кода меняется. Юнит-тесты это не проверка кода на баги: «о, привет, если передать null, код падает»; это не интеграционные тесты, проверяющие всё приложение в сборе; это не регресионные тесты, нагрузочные тесты или тестирование юзабилити. Юнит-тесты это фиксация поведения и дизайна кода или даже документирование кода.
  • Тест должен проваливаться только когда поведения тестируемого кода изменилось. Если тест внезапно начинает проваливаться, но код не изменился, это плохой тест. Хуже него может быть только тест, который проваливается иногда.
  • Юнит-тест не должен зависить от внешних сервисов, зависимостей проверямого кода, порядка исполнения тестов, контекста исполнения и вообще должен исполняться в памяти и быстро. Быстро потому, что в крупном проекте счёт юнит-тестов идёт на тысячи и десятки тысяч и ждать исполнения всех тестов после каждой правки никому не хочется.
  • Тест должен быть фальсифицируемым: должно (пусть и гипотетически) существовать такое входное значение для проверяемого кода, чтобы при неизменном выходном значении тест провалился. Здесь не обойтись без примеров:
На функцию sum()  можно написать два теста:
Первый тест удовлетворяет условию фальсифицируемости: если заменить sum(2,2)  на sum(3,3) , тест провалится. Однако придумать замену для второго теста, чтобы он провалился, не представляется возможным. Замена может быть и умозрительной, например для примера теории о положительности чётных степеней натуральных чисел, такую замену придумать несложно (i*i=-1 где i — мнимая единица), но реализовать не получится — тип Integer не комплексный.

Как устроен хороший юнит-тест

  • Одна проверка на один тест. Для этого есть несколько причин: во-первых обычно юнит-тест завершается на первой же провалившейся проверке, тем самым скрывая будущие провалы последующих проверок. Во-вторых так проще обеспечивать идентичность и нетронутость тестовых данных. В третьих, так проще отлаживать. В четвёртых для простых функций можно писать и больше проверок на один тест, если это приемлимо.
  • Тест использует возможности тестового фреймворка для сброса данных в предопределённое состояние перед запуском. Тест не использует результаты других тестов и не полагается на изменения в тестовых данных, которые они вносят. И на порядок исполнения тоже не полагается 🙂
  • Тест проверяет только код, с которым взаимодействует напрямую. Никаких вызовов методов из других классов быть не должно: исполнятся должен только код класса, метод которого тестируется, все остальные зависимости заменены дублёрами.
  • Тест не проверяет те аспекты кода, которые не влияют на его поведение (если только вы не тестируете именно эти аспекты). Пример:
При написании этого теста нет необходимости проверять, что там в log.trace()  передали, потому что на поведение функции это не влияет (если только вы не отлаживаете log.trace())

  • Аргументы проверочных функций в тесте не перепутаны местами: в JUnit assert* функциях первый параметр — образец, второй — проверяемое значение. В assertThat наоборот: первый параметр — проверяемое значение, второй — матчер.
  • Имя теста отражает суть теста. Полезно выбрать соглашение о именовании тестов и придерживаться его. Хороший пример:  testNameIsEmpty , testNameIsTooLong , testNameIsNull, плохой пример: noName , test128 , nin
  • Хороший тест не обрабатывает исключения сам, а полагается на фреймворк. Если надо провалить тест, когда код выбрасывает исключение — фреймворк поможет. Если наоборот, тест не надо проваливать, когда код выбрасывает исключение — фреймворк умеет и это. Если нужные данные по исключению, сообщение или stacktrace — фреймворк автоматически сделает всё за вас.
  • Тест не тестирует конфигурацию или среду исполнения: это не код, а следовательно не входит в область применения юнит-тестов.
  • Хороший тест не начинается с @Ignore

Для какого кода писать юнит-тесты

Если вы практикуете test driven development, этот вопрос не имеет смысла: тесты появляются ещё раньше кода. Впрочем, заметная часть разработчиков пишет тесты post faсtum или покрывает тестами более старый код. В этом случае:

  • Пишите тесты для того кода, в будущем поведении которого вы не уверены.
  • Ориентируйтесь на цикломатическую сложность кода, чем она выше, тем лучший это кандидат на тестирование.
  • Нет необходимости проверять, как работает компилятор. Функции обёртки, геттеры/сеттеры и прочая можно тестировать в последнюю очередь или не тестировать вовсе:
  • Не тестируйте напрямую private методы, тестируйте их путём вызова прочих методов класса, вызывающих private методы. Если найдёте метод, который никто не вызывает, задумайтесь о его предназначении.
  • Равно и с обычными методами — проверяйте либо результат вызова метода, либо, если метод ничего не возвращает, его побочные эффекты (=поведение). Если ни того, ни другого не обнаружено, задуматесь, а для чего вообще этот метод нужен.
  • При написании тестов для класса двигайтесь от более простых методов к более сложным. Тогда при написании тестов для сложных методов вы сможете воспользоваться наработками от тестов простых методов.
  • При выборе что проверять — результат или поведение, склоняйтесь в сторону результата. Но нет ничего плохого, чтобы смешивать оба подхода (даже в одном тесте).
  • Если к вашему коду не получается написать тест, это признак говнокода и хорошего кандидата на рефакторинг.
  • И самое главное — лучше плохой тест, чем никакого теста.