Pull to refresh

Тесты для тестов

Reading time5 min
Views20K
Один из самых частых ответов на вопрос «Почему я не пишу юнит-тесты» — это вопрос «А кто напишет тесты для моих тестов? Где гарантия, что в моих тестах тоже не будет ошибки?», что свидетельствует о серьёзном недопонимании сути юнит-тестов.

Цель этой заметки — коротко и чётко зафиксировать этот момент, чтобы больше не возникало разногласий.

Итак, юнит-тест — это набор из нескольких примеров, показывающих, что приходит на вход вашей программе, и что получается на выходе. Например, если программа вычисляет длину гипотенузы, то в юнит-тесте достаточно одной строчки:
 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:
5.97 КБ

Ошибки

Из-за непонимая сути юнит-тестов при их написании часто допускаются главные ошибки, делающие юнит-тесты бесполезными, трудоёмкими и приводящие к бесконечным спорам о том, нужны ли вообще юнит-тесты.

Первая типичная ошибка — это не проверять вообще ничего. Например:

	@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...

Tags:
Hubs:
+65
Comments111

Articles