JUnit 4. Перезагрузка

JUnit – это самый популярный в настоящее время инструмент модульного тестирования приложений в мире Java. Существует ряд других достаточно мощных инструментов, например, TestNG, но надо признать, что им пока не удалось достигнуть такой широкой популярности у разработчиков, как JUnit. С выходом четвертой версии, Кент Бэк (Kent Beck) и Эрик Гамма (Eric Gamma) впервые за последние несколько лет представили значительные изменения программного интерфейса (API). В то время, когда свет увидел первый релиз проекта, в 2005ом году, его было не так просто использовать в существующих проектах из-за отсутствия достаточной поддержки в инструментах для разработки приложений. На данный момент абсолютное большинство инструментов автоматической сборки приложений и средств разработки (IDE) включают полную поддержку JUnit 4. Поэтому самое время испытать его в действии. В этой статье мы рассмотрим новшества JUnit 4 по отношению к версии 3.8.x.

Итак, что же нового в JUnit 4?


Начнем с самого очевидного: JUnit 4 полностью построен на аннотациях. Многие спросят: И и что же, собственно, это значит? Во-первых, вам больше не нужно наследовать класс TestCase. Потом, имена ваших тест-методов не обязательно должны начинаться префиксом test. Все, что вам нужно сделать, – это обозначить тестовый метод с помощью аннотации @Test. Например, рРассмотрим следующий пример:

Раньше было так:

import junit.framework.TestCase;
public class CalculatorTest extends TestCase {
public void testadd() {
....
}
}

Теперь нужно писать так:

import org.junit.Test;
public class CalculatorTest {
@Test
public void add() {
....
}
}

"Хорошо, возможно, отсутствие необходимости наследовать класс TestCase может быть полезно в некоторых случаях. А как же быть с методами assert…, которые я использовал, наследуя их у этого класса? "

Здесь нам помогает очередное новшество из Java 5: статические импорты.

import org.junit.Test;
import static org.junit.Assert.*;

public class CalculatorTest {

@Test
public void add() {
...
assertEquals( 4, calculator.add( 1, 3 ) );
}
}

Настраиваем тестовую среду

Итак, мы убедились, что написание тестов с помощью JUnit 4 изменилось не слишком сильно. "Подождите, а что случилось с методами setUp() и tearDown()?" Здесь все так же просто: теперь вы можете использовать для этого любой из ваших методов, промаркировав его с помощью аннотаций @Before или @After:

public class CalculatorTest {

@Before
public void prepareTestData() { ... }

@After
public void cleanupTestData() { ... }
}

Более того, вы можете промаркировать одновременно несколько методов @Before и @After. Но при этом следует иметь в виду, что ничего нельзя сказать ничего конкретного про порядок, в котором эти методы будут вызываться:

public class CalculatorTest {

@Before
public void prepareTestData() { ... }

@Before
public void setupMocks() { ... }

@After
public void cleanupTestData() { ... }
}

Одна вещь, о которой стоит упомянуть, – это то, что унаследованные методы @Before и @After выполняются симметрично. Это значит, что, если у нас есть тестовый класс ScientificCalculatorTest, который наследует класс CalculatorTest, они будут выполнены в следующем порядке:

CalculatorTest#Before
ScientificCalculatorTest#Before
ScientificCalculatorTest#After
CalculatorTest#After

Есть еще одна возможность, которую многие разработчики раньше не могли использовать, – это возможность определять методы setup/teardown для целого набора тестов. Это бывает полезно в случае, когда ваш метод настройки выполняет требовательные к ресурсам и времени операции перед вызовом каждого теста, - например, устанавливает соединение с базой данных. Сейчас это можно сделать с помощью аннотаций @BeforeClass и @AfterClass:

public class CalculatorTest {

@BeforeClass
public static void setupDatabaseConnection() { ... }

@AfterClass
public static void teardownDatabaseConnection() { ... }
}

Здесь действуют те же правила, что и в случае с аннотациями @Before и @After. Т.е. вы можете иметь сразу несколько методов, помеченных аннотациями @BeforeClass и @AfterClass. Обратите внимание, :что эти методы должны быть статическими.

Тестируем исключения

Проверка того, что код выдает правильные результаты – это одно. Но как быть с обработкой ошибок? Обычно, когда что-то идет не так, выбрасывается исключение (exception). Тестирование того, что код корректно работает в исключительных ситуациях, не менее важно (а, может быть, и более важно), чем тестирование реальной функциональности этого кода. И это одно из самых важных достоинств unit-тестов: вы можете искусственно поставить свой код в контролируемую исключительную ситуацию и проверить, или выбрасывается ли ожидаемое исключение. В JUnit версии 3.8.x шаблон тестирования исключений был следующим:

public class CalculatorTest {

public void testDivisionByZero() {
try {
new Calculator().divide( 4, 0 );
} catch (ArithmeticException e) {}
}
}

В JUnit 4 вам нужно лишь объявить ожидаемое исключение в аннотации @Test:

public class CalculatorTest {

@Test(expected=ArithmeticException.class)
public void testDivisionByZero() {
new Calculator().divide( 4, 0 );
}
}

Тестирование с использованием Timeout

Выполнение unit-тестов должно занимать как можно меньше времени, поскольку предполагается, что они будут запускаться довольно часто. Но в некоторых ситуациях тесты могут выполняться дольше, чем предполагалось. Например, когда тесты используют сетевые соединения. Поэтому, когда вы предполагаете, что выполнение теста может занять больше времени, чем необходимо, было бы неплохо задать какой-то максимальный временной промежуток, по истечении которого выполнение этого теста будет прекращено. С JUnit 3.8.x для этого приходилось прибегать к помощи сторонних библиотек. Или еще хуже – писать все самим с использованием новых потоков (Threads). Теперь все стало намного проще; : все, что нужно сделать, – это указать параметр timeout с необходимым значением в аннотации @Test:

@Test(timeout=5000)
public void testLengthyOperation() {
...
}

Если максимальное отведенное тесту время истекает, то мы получаем понятное сообщение об ошибке и о не выполнении теста:

java.lang.Exception: test timed out after 5000 milliseconds

Игнорирование тестов

В некоторых ситуациях может понадобиться отключить некоторые тесты. Например, возможно, в текущей версии используемой вами библиотеки имеется ошибка, или по какой-то причине определенный тест не может быть выполнен в текущей среде. Что бы ни было в JUnit 3.8.x, чтобы отключить тесты, приходилось их комментировать. В JUnit 4 для этих целей вам нужно просто промаркировать игнорируемый тест с помощью аннотации @Ignore.
public class CalculatorTest {

@Ignore("Not running because <fill in a good reason here>")
@Test
public void testTheWhatSoEverSpecialFunctionality() {
}
}

Текст, который передается в аннотации @Ignore, будет выводиться при выполнении тестов. Указывать текст не обязательно, но очень полезно всегда задавать сообщение для того, чтобы позже не забыть про то, что этот тест отключен. TestRunner, встроенный в Eclipse, маркирует игнорируемые тесты визуально, зачеркивая их имена.

Наборы тестов

Старый добрый метод suit() также был заменен аннотацией в JUnit 4. Теперь, вместо

public class AllTests extends TestCase {

public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTestSuite(CalculatorTest.class);
suite.addTestSuite(AnotherTest.class);
return suite;
}

вы должны писать:

@RunWith(value=Suite.class)
@SuiteClasses(value={CalculatorTest.class, AnotherTest.class})
public class AllTests {
...
}

JUnit 4 вводит возможность определения различных классов для запуска тестов. В нашем случае, мы сказали JUnit с помощью аннотации @RunWith, что мы хотим использовать класс Suite для запуска этих тестов. В свою очередь, аннотация @SuiteClasses используется классом Suite для определения тестов, которые входят в данный набор. Одно из достоинств нового подхода к определению наборов тестов – это возможность использования методов, аннотированных с помощью @BeforeClass и @AfterClass. После этого эти данные методы будут вызываться перед началом запуска всех тестов и после выполнения последнего теста из набора соответственно. Таким образом, вы сможете выполнить настройку необходимых параметров сразу для целого набора тестов.

Параметризированные тесты

Помимо класса Suite для запуска тестов, в JUnit 4 есть еще специальный класс: Parameterized. Он позволяет запускать одни и те же тесты, но с разными наборами данных. Рассмотрим это на примере. Напишем пример теста для метода, который вычисляет факториал заданного числа n:
@RunWith(value=Parameterized.class)
public class FactorialTest {

private long expected;
private int value;

@Parameters
public static Collection data() {
return Arrays.asList( new Object[][] {
{ 1, 0 }, // expected, value
{ 1, 1 },
{ 2, 2 },
{ 24, 4 },
{ 5040, 7 },
});
}

public FactorialTest(long expected, int value) {
this.expected = expected;
this.value = value;
}

@Test
public void factorial() {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.factorial(value));
}
}

В нашем примере класс Parameterized запускает все тесты класса FactorialTest (у нас в примере он только один), используя при этом данные методов, промаркированных аннотацией @Parameters. В данном случае мы имеем список с пятью элементами. Каждый элемент содержит массив, который будет использован в качестве аргументов конструктора класса FactorialTest. Наш тест factorial() использует эти данные при вызове метода assertEquals(). В итоге это означает, что наш тест будет выполнен 5 раз с использованием следующих данных:

factorial#0: assertEquals( 1, calculator.factorial( 0 ) );
factorial#1: assertEquals( 1, calculator.factorial( 1 ) );
factorial#2: assertEquals( 2, calculator.factorial( 2 ) );
factorial#3: assertEquals( 24, calculator.factorial( 4 ) );
factorial#4: assertEquals( 5040, calculator.factorial( 7 ) );

Различные дополнения

Все новые классы JUnit теперь располагаются в новом пакете: org.junit. Старый фреймуорк все еще находится в пакете junit.framework для совместимости. Также одно из небольших изменений: вместо исключения junit.framework.AssertionFailedError, в новой версии выбрасывается исключение (java.lang.)AssertionError.

Еще одно очень полезное дополнение касается нового метода assert для проверки на соответствие массивов. Этот метод сначала проверяет или массивы имеют одинаковое количество элементов, а потом сравнивает элементы массивов с помощью методов equals().

assertEquals(Object[] expected, Object[] actual)

JUnit 4 больше не поддерживает пользовательский интерфейс для TestRunner; - это оставлено разработчикам сред разработки (IDE). Однако все еще в силе инструмент для запуска ваших тестов из командной строки вручную. Для этого необходимо вызвать класс org.junit.runner.JUnitCore и передать имена ваших тестовых классов:

java -cp ... org.junit.runner.JUnitCore CalculatorTest AnotherTest

Резюме

Используя JUnit 4, мы получаем: полную поддержку аннотаций, значительные улучшения в настройке тестов, расширенная расширенную поддержка поддержку новых классов для запуска тестов, а также очень нужный метод assertEquals() для сравнения массивов. Однако многие могут сказать, что многие фреймуорки, например, TestNG, уже давно содержат похожие функции. Это так, но, так или иначе, JUnit на сегодня - самый широко используемый инструмент для модульного тестирования приложений. Как следствие этого – каждый крупный инструмент разработки приложений имеет встроенную поддержку JUnit. При этом нет необходимости устанавливать дополнительные плагины. В общем, хватит слов, - самое время попробовать новую версию JUnit в действии!

Ресурсы

1. JUnit.org: ( сайт )
2. JUnit 4 Extensions: ( сайт )
3. TestNG: ( сайт )

По материалам Ralf Stuckert

Алексей Литвинюк, http://www.litvinyuk.com


Компьютерная газета. Статья была опубликована в номере 06 за 2007 год в рубрике программирование

©1997-2024 Компьютерная газета