Новая жизнь. Web-студия Татьяны Самойловой.

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

Опубликовано Янв 31, 2011 в Блог, Новости web


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

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

Итак, юнит-тест — это набор из нескольких примеров, показывающих, что приходит на вход вашей программе, и что получается на выходе. Например, если программа вычисляет длину гипотенузы, то в юнит-тесте достаточно одной строчки:

 assertEquals(5, hypotenuse(3, 4));

Эти тестовые значения вы должны придумать сами: посчитать столбиком на бумажке или на калькуляторе.

Правила

Итак, юнит-тест отличается от программы тем, что:

  • Он на порядок проще, чем тестируемая программа
  • В нём только набор примеров
  • В нём нет той логики, что есть в программе (примеры вы придумываете сами)
  • Юнит-тест не может и не должен покрывать все возможные случаи. Скорее он должен обозначить все важные случаи, то есть случаи, которые следует отдельно обговорить.
  • Юнит-тест должен служить документацией к программе — то есть прочитав юнит-тест, человек должен понять, как работает программа. Обычно для этой цели используются названия тест-методов.

Первый пункт и есть ответ на вопрос «А кто напишет тесты для моих тестов?». Поскольку юнит-тест на порядок проще, чем программа, то тест для него пришлось бы писать ещё на порядок проще, но это практически нереально, поскольку он и так до крайности простой.

Ответ на второй вопрос «Где гарантия, что в моих тестах тоже не будет ошибки?» такой: гарантии нет и быть не может. Но тот факт, что «правильные» ответы, прописанные в тесте, получены другим путём, нежели ответы, выдаваемые программой, позволяет быть более уверенным, что оба пути правильные.

Пример

Давайте попробуем привести здесь минимальный пример юнит-теста, иллюстрирующий всё вышесказанное. Допустим, нам надо написать функцию, решающую, является ли данный год високосным. Напомню, високосный год — это год, который делится на 4, за исключением тех лет, которые делятся на 100, но не делятся на 400.

import org.junit.Test;

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;

public class LeapYear
{
	public static boolean isLeap(int year) {
		return year % 4 == 0  year % 100 != 0 || year % 400 == 0;
	}

	@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; i100000; i++)  {
			assertEquals(i % 4 == 0  i % 100 != 0 || i % 400 == 0, isLeap(i));
		}
	}

Этот юнит-тест плох как раз по той причине, что в нём программист может допустить те же ошибки, что и в самом коде.

Smells

Признаки того, что юнит-тест неправильный:

  • Условия и циклы
    Если вы видите в юнит-тесте условия и циклы — это явный признак того, что тест неправильный. Попробуйте избавиться от циклов и написать несколько простых примеров.
  • Название тест-метода не говорит, что и как должно работать.
    Если вы видите в названии только название того, что тестируется — например, testLeapYear, будьте настороже. Вероятно, он не тестирует ничего либо тестирует слишком много. Правильное название тест-метода должно звучать примерно так: «в таких-то условиях такой-то метод должен вести себя так-то и так-то».
  • В теле тест-метода слишком много assert.
    Такой тест-метод проверяет слишком много аспектов. Если он сломается, по названию метода невозможно будет сразу определить, в чём ошибка — вам придётся анализировать код тест-класса. Попробуйте разбить тест-метод на несколько методов и дать им говорящие названия.
  • Какие ещё признаки знаете вы?..

Open source

Всегда полезно поизучать юнит-тесты известных open-source проектов. Эта тема достойна отдельной статьи, но вот первое, куда что пришло в голову — это Apache Velocity и Spring Framework. Тест из первого проекта UnicodeInputStreamTestCase мне не понравился, так как названия тест-методов не очень-то описывают поведение программы (например, «testSimpleStream()»). А вот тест для Spring мне понравились, например, Например, для класса CollectionUtils есть юнит-тест CollectionUtilsTests, а для класса Assert есть юнит-тесты AssertTests.

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

To test or not to test — that is not a question…

Читатели рекомендуют прочесть:



Оставить комментарий