До сих пор мы тестировали только корректное поведение кода (happy path) — проверяли корректную работу на корректных данных. Такой идеальный мир, к сожалению, встречается только в учебниках, да и то, не во всех. Реальный код сталкивается с кривыми руками программистов данными и ошибками, и должен на них реагировать. Традиционный способ обработки ошибок в Java — исключения и их тоже нужно тестировать.
Подготовка
Возьмём код из примера основной функциональности JUnit и создадим новый класс с тестами в StringUtilsExceptionsTest.
Простая проверка исключений
Один из методов в StringUtils, toDouble(String), бросает NumberFormatException, если передать в него неправильную строку. Это поведение можно и нужно протестировать:
1
2
3
4
|
@Test(expected = NumberFormatException.class)
public void testToDoubleException() {
StringUtils.toDouble(testString);
}
|
Параметер expected говорит JUnit что метод должен кинуть исключение указанного типа и это не будет ошибкой теста. А вот если исключение не будет брошено, это будет ошибкой теста.
Простой метод проверки исключений имеет, впрочем, некоторое количество недостатков: во-первых, проверяется только сам факт исключения, но нет возможности проверить связанную с исключением информацию. Второй недостаток следует из первого — нет возможности проверить, какая именно часть кода кинула исключение, в случае если исключение того же самого типа может быть выброшено из разных мест тестируемого метода.
Matchers, Rules, Exceptions
В JUnit предусмотрен более сложный подход, основаный на гибкой и расширяемой системе Rules(правил), которой будет посвящена отдельная статься. Однако воспользоваться одним из Rule, для подробной проверки исключений, достаточно несложно:
1
2
3
4
5
6
7
8
9
10
11
12
|
@Rule
public ExpectedException exception = ExpectedException.none();
/* ..... */
@Test
public void testToDoubleExceptionDeepCheck() {
exception.expect(NumberFormatException.class);
exception.expectMessage(containsString(testString));
StringUtils.toDouble(testString);
}
|
Проверка исключений с использованием ExpectedException состоит из двух частей: создание проверяющего Rule и его настройка. Конструируется ExpectedException достаточно очевидным методом:
1
2
|
@Rule
public ExpectedException exception = ExpectedException.none();
|
Важно, чтобы экземпляр объект Rule был объявлен как public, поскольку JUnit использует reflection для вызова кода Rule.
Настройка Rule производится к каждом тесте отдельно. Состояние ExpectedException сбрасывается перед каждым тестом (об этом говорит аннотация @Rule). Можно указать какое именно исключение ожидается, проверить его поля message и cause.
Проверочные значения можно задавать как напрямую, так и используя Matcher’ы, что делает проверку ещё гибче:
1
2
3
4
5
6
7
|
@Test
public void testToDoubleExceptionDeepCheck() {
exception.expect(NumberFormatException.class);
exception.expectMessage(containsString(testString));
StringUtils.toDouble(testString);
}
|
try/catch
В совсем сложных случаях, когда, например, надо проверить содержимое исключения вашего собственного типа, можно использовать существующий ещё с JUnit3 подход try/catch/fail (а можно расширить ExpectedException). В этом случае вы вручную перехватываете исключение, делаете с ним всё что пожелаете, а если исключение не было выкинуто, вручную же проваливаете тест:
1
2
3
4
5
6
7
8
9
|
@Test
public void testToDoubleExceptionManual() {
try {
StringUtils.toDouble(testString);
fail("Expected NumberFormatException");
} catch (NumberFormatException ex) {
assertThat(ex.getMessage(), containsString(testString));
}
}
|
try/catch часть кода очевидна — вызывается метод, который кидает исключение, исключение перехватывается и тестируется. Но, после вызова метода стоит вызов fail(), который вручную проваливает тест, если исключение не было брошено. Разумеется, fail() можно использовать в любом удобном случае, когда требуется провалить тест вручную.
Исходный код примера доступен на github