Один из самых частых ответов на вопрос «Почему я не пишу юнит-тесты» — это вопрос «А кто напишет тесты для моих тестов? Где гарантия, что в моих тестах тоже не будет ошибки?», что свидетельствует о серьёзном недопонимании сути юнит-тестов.
Цель этой заметки — коротко и чётко зафиксировать этот момент, чтобы больше не возникало разногласий.
Итак, юнит-тест — это набор из нескольких примеров, показывающих, что приходит на вход вашей программе, и что получается на выходе. Например, если программа вычисляет длину гипотенузы, то в юнит-тесте достаточно одной строчки:
Итак, юнит-тест отличается от программы тем, что:
Ответ на второй вопрос «Где гарантия, что в моих тестах тоже не будет ошибки?» такой: гарантии нет и быть не может. Но тот факт, что «правильные» ответы, прописанные в тесте, получены другим путём, нежели ответы, выдаваемые программой, позволяет быть более уверенным, что оба пути правильные.
UPD: kalantyr подсказывает, что юнит-тесты и код тестируют друг друга, так как они взаимосвязаны. То есть в некотором смысле тестами для тестов является сам тестируемый код.
Давайте попробуем привести здесь минимальный пример юнит-теста, иллюстрирующий всё вышесказанное. Допустим, нам надо написать функцию, решающую, является ли данный год високосным. Напомню, високосный год — это год, который делится на 4, за исключением тех лет, которые делятся на 100, но не делятся на 400.
Как видите, внутри самих тест-кейсов — конкретные простые примеры: 2000, 2008, 1011. Я их придумал сам, из головы. А названия тест-кейсов их поясняют (обобщают) на человеческом языке. Заметьте, названия тест-методов один-в-один совпадают с описанием термина «високосный год», приведённого выше. Так и должно быть: тест должен читаться как документация. А вот так он выглядит, к примеру, в IDEA:
Первая типичная ошибка — это не проверять вообще ничего. Например:
Несмотря на кажущуюся абсурдность, именно с таких тестов начинают все, кто впервые берут в руки JUnit.
Вторая типичная ошибка — это попытаться повторить в тесте ту же логику, что есть в программе. Например:
Признаки того, что юнит-тест неправильный:
Всегда полезно поизучать юнит-тесты известных open-source проектов. Эта тема достойна отдельной статьи, но вот первое, куда что пришло в голову — это Apache Velocity и Spring Framework. Тест из первого проекта UnicodeInputStreamTestCase мне не понравился, так как названия тест-методов не очень-то описывают поведение программы (например, «testSimpleStream()»). А вот тест для Spring мне понравились, например, Например, для класса CollectionUtils есть юнит-тест CollectionUtilsTests, а для класса Assert есть юнит-тесты AssertTests.
Надеюсь, эта заметка прекратит вечные споры о том, что юнит-тесты бесполезны, трудоёмки и пр., и сможет послужить отправной точкой для дальнейших дискуссий о том, как писать, сколько писать, на чём писать и так далее. А кого-то, быть может, даже надоумит оторваться от хабра и почитать серьёзные книжки, такие как "Test Driven Development (Kent Beck)", "Шаблоны тестирования xUnit (Gerard Meszaros)" и мою любимую "Clean Code (Robert C. Martin)".
To test or not to test — that is not a question...
Цель этой заметки — коротко и чётко зафиксировать этот момент, чтобы больше не возникало разногласий.
Итак, юнит-тест — это набор из нескольких примеров, показывающих, что приходит на вход вашей программе, и что получается на выходе. Например, если программа вычисляет длину гипотенузы, то в юнит-тесте достаточно одной строчки:
Эти тестовые значения вы должны придумать сами: посчитать столбиком на бумажке или на калькуляторе.assertEquals(5, hypotenuse(3, 4));
Конечно, достаточно ли этого теста — решать вам. Если в вашей конкретной задаче очень важно, чтобы функция правильно работала на очень маленьких числах, то следует добавить и отдельный тестбывает важно также описать поведение функции в случае отрицательных или ещё каких-то параметров. В общем, решать вам, но основная идея в том, что в юнит-тесте должно быть несколько важных примеров.assertEquals(0.00005, hypotenuse(0.00003, 0.00004));
Правила
Итак, юнит-тест отличается от программы тем, что:
- Он на порядок проще, чем тестируемая программа
- В нём только набор примеров
- В нём нет той логики, что есть в программе (примеры вы придумываете сами)
- Юнит-тест не может и не должен покрывать все возможные случаи. Скорее он должен обозначить все важные случаи, то есть случаи, которые следует отдельно обговорить.
- Юнит-тест должен служить документацией к программе — то есть прочитав юнит-тест, человек должен понять, как работает программа. Обычно для этой цели используются названия тест-методов.
Ответ на второй вопрос «Где гарантия, что в моих тестах тоже не будет ошибки?» такой: гарантии нет и быть не может. Но тот факт, что «правильные» ответы, прописанные в тесте, получены другим путём, нежели ответы, выдаваемые программой, позволяет быть более уверенным, что оба пути правильные.
UPD: kalantyr подсказывает, что юнит-тесты и код тестируют друг друга, так как они взаимосвязаны. То есть в некотором смысле тестами для тестов является сам тестируемый код.
Пример
Давайте попробуем привести здесь минимальный пример юнит-теста, иллюстрирующий всё вышесказанное. Допустим, нам надо написать функцию, решающую, является ли данный год високосным. Напомню, високосный год — это год, который делится на 4, за исключением тех лет, которые делятся на 100, но не делятся на 400.
public class LeapYear
{
public static boolean isLeap(int year) {
return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
}
}
import org.junit.Test;
import static junit.framework.Assert.*;
public class LeapYearTest
{
@Test
public void yearDividing4IsLeap() throws Exception
{
assertTrue(isLeap(2008));
assertTrue(isLeap(2012));
assertFalse(isLeap(2011));
}
@Test
public void exceptYearDividing100WhichIsNotLeap() throws Exception
{
assertFalse(isLeap(1900));
assertFalse(isLeap(2100));
}
@Test
public void exceptYearDividing400WhichIsLeap() throws Exception
{
assertTrue(isLeap(1600));
assertTrue(isLeap(2000));
}
}
Как видите, внутри самих тест-кейсов — конкретные простые примеры: 2000, 2008, 1011. Я их придумал сам, из головы. А названия тест-кейсов их поясняют (обобщают) на человеческом языке. Заметьте, названия тест-методов один-в-один совпадают с описанием термина «високосный год», приведённого выше. Так и должно быть: тест должен читаться как документация. А вот так он выглядит, к примеру, в IDEA:
Ошибки
Из-за непонимая сути юнит-тестов при их написании часто допускаются главные ошибки, делающие юнит-тесты бесполезными, трудоёмкими и приводящие к бесконечным спорам о том, нужны ли вообще юнит-тесты.Первая типичная ошибка — это не проверять вообще ничего. Например:
@Test
public void testLeapYear() throws Exception
{
int year = 2004;
assertEquals(2004, year);
}
Этот юнит-тест бессмысленный, так как он не проверяет нашу программу. Максимум, что он проверяет — это что Java работает правильно, но это уже паранойя. Пусть этим занимается Oracle.Несмотря на кажущуюся абсурдность, именно с таких тестов начинают все, кто впервые берут в руки JUnit.
Вторая типичная ошибка — это попытаться повторить в тесте ту же логику, что есть в программе. Например:
@Test
public void testLeapYear() throws Exception
{
for (int i=0; i<100000; i++) {
assertEquals(i % 4 == 0 && i % 100 != 0 || i % 400 == 0, isLeap(i));
}
}
Этот юнит-тест плох как раз по той причине, что в нём программист может допустить те же ошибки, что и в самом коде. Smells
Признаки того, что юнит-тест неправильный:
- Условия и циклы
Если вы видите в юнит-тесте условия и циклы — это явный признак того, что тест неправильный. Попробуйте избавиться от циклов и написать несколько простых примеров.
Конечно, циклы иногда нужны и в тесте, например, чтобы сравнить два массива. Скорее имелось в виду, что не должно быть циклов с такой же логикой, как в тестируемой программе. - Название тест-метода не говорит, что и как должно работать.
Если вы видите в названии только название того, что тестируется — например, testLeapYear, будьте настороже. Вероятно, он не тестирует ничего либо тестирует слишком много. Правильное название тест-метода должно звучать примерно так: «в таких-то условиях такой-то метод должен вести себя так-то и так-то». - В теле тест-метода слишком много assert.
Такой тест-метод проверяет слишком много аспектов. Если он сломается, по названию метода невозможно будет сразу определить, в чём ошибка — вам придётся анализировать код тест-класса. Попробуйте разбить тест-метод на несколько методов и дать им говорящие названия.
Конечно, иногда в тест-методе может быть много assert, например, для того, чтобы проверить, что большом HTML присутствуют все необходимые значения. Скорее эту рекомендацию следует понимать как «делать тесты покороче и проверять один возможный путь в одном методе, чтобы было легче локализовать место падения теста.» - Какие ещё признаки знаете вы?..
Open source
Всегда полезно поизучать юнит-тесты известных open-source проектов. Эта тема достойна отдельной статьи, но вот первое, куда что пришло в голову — это Apache Velocity и Spring Framework. Тест из первого проекта UnicodeInputStreamTestCase мне не понравился, так как названия тест-методов не очень-то описывают поведение программы (например, «testSimpleStream()»). А вот тест для Spring мне понравились, например, Например, для класса CollectionUtils есть юнит-тест CollectionUtilsTests, а для класса Assert есть юнит-тесты AssertTests.
Надеюсь, эта заметка прекратит вечные споры о том, что юнит-тесты бесполезны, трудоёмки и пр., и сможет послужить отправной точкой для дальнейших дискуссий о том, как писать, сколько писать, на чём писать и так далее. А кого-то, быть может, даже надоумит оторваться от хабра и почитать серьёзные книжки, такие как "Test Driven Development (Kent Beck)", "Шаблоны тестирования xUnit (Gerard Meszaros)" и мою любимую "Clean Code (Robert C. Martin)".
To test or not to test — that is not a question...