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