4 мая 2014 г.

Тестирование исключений в JUnit

Хорошим тоном при разработке ПО считается создавать для любой исключительной ситуации в своём приложении базовое исключение и все ваши производные исключения (если таковые нужны) наследовать именно от него, а не от класса Exception. Давайте создадим такой класс.

package developer.remarks;

public class CustomException extends Exception {

public CustomException(String message) {
super(message);
}
}

Набор конструкторов вы должны определить, исходя из своих потребностей. Возможно, в вашем исключении появятся дополнительные поля, несущие служебную информацию об ошибке. Теперь метод, в котором может произойти ваш CustomException будет выглядеть как-то так:

package developer.remarks;

public class BusinessLogic {

public void doWork() throws CustomException {
throw new CustomException("произошла ошибка");
}
}

Тело метода doWork в данном случае принудительно вбрасывает исключение для наглядности примера.

А как в JUnit протестировать, что при определённых условиях ваш метод действительно выкинет CustomException? Самый простой способ - это указание ожидаемого исключения в аннотации @Test(expected = ...).

package developer.remarks;

import org.junit.Test;

public class ExceptionTest {

@Test(expected = CustomException.class)
public void testException() throws CustomException {
BusinessLogic logic = new BusinessLogic();
logic.doWork();
}
}

Обратите внимание, что в сигнатуре метода testException() обязательно нужно объявить наше исключение. Теперь тест будет считаться пройденным, если в процессе выполнения данного метода будет выброшено исключение CustomException. Также заметьте, что строки после вызова метода doWork в данном тесте выполнены не будут.

Очень хорошо, такой подход позволяет нам протестировать сам факт возникновения определённого исключения. Но как быть, если нам нужно также проверить параметры этого исключения, например, строку сообщения об ошибке? Это делается при помощи правил JUnit и класса ExpectedException (работает для JUnit, начиная с версии 4.7).

Убираем из аннотации @Test параметр expected и добавляем в тестовый класс поле expectedException с модификатором public:

@Rule
public ExpectedException expectedException = ExpectedException.none();

В итоге наш тестовый класс будет выглядеть так:

package developer.remarks;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class ExceptionTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void testException() throws CustomException {
expectedException.expect(CustomException.class);
expectedException.expectMessage("произошла ошибка");
BusinessLogic logic = new BusinessLogic();
logic.doWork();
}
}

Теперь перед вызовом целевого метода вы можете указывать, класс какого исключения ожидается и с каким сообщением. Поле expectedException можно использовать для разных тестовых методов в рамках одного класса, но лишь один раз в самом методе, что логично, т.к. после возникновения исключения выполнение метода прерывается.

Ну и напоследок небольшой совет, как быть, если в тексте ошибки содержится служебная информация, которую тестировать нам необязательно. Например, время возникновения ошибки ("04.05.2014 произошла ошибка"). Тогда мы можем протестировать только наличие смысловой части сообщения при помощи JUnitMatchers (т.е. наличие подстроки в сообщении):

expectedException.expectMessage(JUnitMatchers.containsString(" произошла ошибка"));

Таким образом, JUnit позволяет легко и просто тестировать не только сам факт возникновения ошибки, но и её описание.

3 мая 2014 г.

Тестирование System.out.print

Актуальная версия статьи на моём новом сайте devmark.ru

Пусть у вас есть метод, который выводит сообщение на экран и вам необходимо его протестировать.

package developer.remarks;

public class BusinessLogic {

public void doWork() {
System.out.print("some text");
}
}

Как протестировать текст сообщения, выведенный на экран, ведь данный метод ничего не возвращает? Предлагаю создать вспомогательный абстрактный класс, в котором перед началом тестирования происходит перенаправление стандартного потока вывода, а после тестирования всё возвращается на изначальные позиции.

package developer.remarks;

import org.junit.After;
import org.junit.Before;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

public abstract class OutputTest {

protected final ByteArrayOutputStream output = new ByteArrayOutputStream();

@Before
public void setUpStreams() {
System.setOut(new PrintStream(output));
}

@After
public void cleanUpStreams() {
System.setOut(null);
}
}

Здесь метод с аннотацией @Before вызывается перед каждым методом, помеченным аннотацией @Test, а метод с аннотацией @After - после выполнения этого теста. Таким образом, всё, что было выведено на экран, можно получить при помощи output.toString().

Привожу пример такого теста:

package developer.remarks;

import org.junit.Assert;
import org.junit.Test;

public class BusinessLogicTest extends OutputTest {

@Test
public void testString() {
BusinessLogic logic = new BusinessLogic();
logic.doWork();
Assert.assertEquals("some text", output.toString());
}
}

Важно заметить, что если бы мы использовали метод System.out.println в тестируемом методе, то после каждого такого вывода добавлялся бы символ перевода строки ("\n").