Чтобы написать хороший юнит-тест, надо задать критерии его хорошести и придерживаться их. Это, вообще, относится к любой области деятельности: определяем цель, движемся к ней, если результат совпал с целью, значит всё удалось 🙂
Признаки хорошего юнит-теста
- Тест должен проваливаться, когда поведение тестируемого кода меняется. Юнит-тесты это не проверка кода на баги: «о, привет, если передать null, код падает»; это не интеграционные тесты, проверяющие всё приложение в сборе; это не регресионные тесты, нагрузочные тесты или тестирование юзабилити. Юнит-тесты это фиксация поведения и дизайна кода или даже документирование кода.
- Тест должен проваливаться только когда поведения тестируемого кода изменилось. Если тест внезапно начинает проваливаться, но код не изменился, это плохой тест. Хуже него может быть только тест, который проваливается иногда.
- Юнит-тест не должен зависить от внешних сервисов, зависимостей проверямого кода, порядка исполнения тестов, контекста исполнения и вообще должен исполняться в памяти и быстро. Быстро потому, что в крупном проекте счёт юнит-тестов идёт на тысячи и десятки тысяч и ждать исполнения всех тестов после каждой правки никому не хочется.
- Тест должен быть фальсифицируемым: должно (пусть и гипотетически) существовать такое входное значение для проверяемого кода, чтобы при неизменном выходном значении тест провалился. Здесь не обойтись без примеров:
1
2
3
|
public Integer sum(Integer a, Integer b) {
return a+b
}
|
1
2
3
4
5
6
7
8
|
@Test
public void testGoodOne() {
assertThat(sum(2,2), is(4));
}
public void testBadOne() {
assertNotNull(sum(2,2));
}
|
Как устроен хороший юнит-тест
- Одна проверка на один тест. Для этого есть несколько причин: во-первых обычно юнит-тест завершается на первой же провалившейся проверке, тем самым скрывая будущие провалы последующих проверок. Во-вторых так проще обеспечивать идентичность и нетронутость тестовых данных. В третьих, так проще отлаживать. В четвёртых для простых функций можно писать и больше проверок на один тест, если это приемлимо.
- Тест использует возможности тестового фреймворка для сброса данных в предопределённое состояние перед запуском. Тест не использует результаты других тестов и не полагается на изменения в тестовых данных, которые они вносят. И на порядок исполнения тоже не полагается 🙂
- Тест проверяет только код, с которым взаимодействует напрямую. Никаких вызовов методов из других классов быть не должно: исполнятся должен только код класса, метод которого тестируется, все остальные зависимости заменены дублёрами.
- Тест не проверяет те аспекты кода, которые не влияют на его поведение (если только вы не тестируете именно эти аспекты). Пример:
1
2
3
4
5
6
|
public Integer sum(Integer a, Integer b) {
log.trace("Adding {} to {}", a, b);
Integer result = a+b;
log.trace("Result is {}", result);
return result;
}
|
- Аргументы проверочных функций в тесте не перепутаны местами: в JUnit assert* функциях первый параметр — образец, второй — проверяемое значение. В assertThat наоборот: первый параметр — проверяемое значение, второй — матчер.
- Имя теста отражает суть теста. Полезно выбрать соглашение о именовании тестов и придерживаться его. Хороший пример: testNameIsEmpty , testNameIsTooLong , testNameIsNull, плохой пример: noName , test128 , nin
- Хороший тест не обрабатывает исключения сам, а полагается на фреймворк. Если надо провалить тест, когда код выбрасывает исключение — фреймворк поможет. Если наоборот, тест не надо проваливать, когда код выбрасывает исключение — фреймворк умеет и это. Если нужные данные по исключению, сообщение или stacktrace — фреймворк автоматически сделает всё за вас.
- Тест не тестирует конфигурацию или среду исполнения: это не код, а следовательно не входит в область применения юнит-тестов.
- Хороший тест не начинается с @Ignore
Для какого кода писать юнит-тесты
Если вы практикуете test driven development, этот вопрос не имеет смысла: тесты появляются ещё раньше кода. Впрочем, заметная часть разработчиков пишет тесты post faсtum или покрывает тестами более старый код. В этом случае:
- Пишите тесты для того кода, в будущем поведении которого вы не уверены.
- Ориентируйтесь на цикломатическую сложность кода, чем она выше, тем лучший это кандидат на тестирование.
- Нет необходимости проверять, как работает компилятор. Функции обёртки, геттеры/сеттеры и прочая можно тестировать в последнюю очередь или не тестировать вовсе:
1
2
3
|
public List<Item> list() {
return ItemRepository.findAll(); //There is nothing to test here.
}
|
- Не тестируйте напрямую private методы, тестируйте их путём вызова прочих методов класса, вызывающих private методы. Если найдёте метод, который никто не вызывает, задумайтесь о его предназначении.
- Равно и с обычными методами — проверяйте либо результат вызова метода, либо, если метод ничего не возвращает, его побочные эффекты (=поведение). Если ни того, ни другого не обнаружено, задуматесь, а для чего вообще этот метод нужен.
- При написании тестов для класса двигайтесь от более простых методов к более сложным. Тогда при написании тестов для сложных методов вы сможете воспользоваться наработками от тестов простых методов.
- При выборе что проверять — результат или поведение, склоняйтесь в сторону результата. Но нет ничего плохого, чтобы смешивать оба подхода (даже в одном тесте).
- Если к вашему коду не получается написать тест, это признак говнокода и хорошего кандидата на рефакторинг.
- И самое главное — лучше плохой тест, чем никакого теста.