Иногда возникновение исключения является ожидаемым поведением системы, и в тестах нужно проверять, что оно действительно возникает.
Ниже описаны пять способов, как в тестовом фреймворке JUnit перехватить ожидаемое исключение и проверить его свойства. Первые четыре из них можно использовать в JUnit 4, а последний способ использует новые возможности JUnit 5.
В качестве примера для демонстрации возьмём тест для функции стандартной библиотеки, создающей временный файл. Будем проверять, что при попытке создания файла в несуществующей директории возникает исключение типа IOException
. При этом предварительно в том же самом тесте создаётся временная директория и тут же удаляется, так что мы получаем гарантированно несуществующую директорию, в которой и пытаемся создать файл:
Разумеется, в таком виде тест упадёт, а в отчёте будет написано, что возникло исключение. А нам нужно, чтобы тест в этом случае наоборот помечался как успешный. Посмотрим, как это можно исправить.
1. @Test
Самый простой способ сообщить тестовому фреймворку о том, что ожидается исключение – указать дополнительный параметр expected
в аннотации @Test
:
Этот параметр должен содержать тип ожидаемого исключения. Если возникнет исключение именно такого типа – тест пройдёт успешно. Если возникнет исключение другого типа или не возникнет вовсе – тест упадёт.
Достоинства:
- Простота и краткость.
Недостатки:
- Нельзя проверить текст сообщения или другие свойства возникшего исключения.
- Нельзя понять, где именно возникло исключение. В рассматриваемом примере оно могло быть выброшено не тестируемой функцией, а чуть раньше, при попытке создать временную директорию. Тест даже не смог добраться до вызова тестируемой функции – но при этом в отчёте он помечается как успешно пройденный!
Вторая из упомянутых проблем настолько ужасна, что я никому никогда не рекомендую использовать этот способ.
2. try-catch
Оба недостатка можно устранить, если перехватывать исключение явно при помощи конструкции try-catch
:
Если исключение возникает до блока try
– тест падает, мы узнаём о том, что у него возникли проблемы.
Если тестируемая функция не выбрасывает вообще никакого исключения – мы попадаем на fail()
в следующей строке, тест падает.
Если она выбрасывает исключение неподходящего типа – блок catch
не ловит его, тест опять таки падает.
Успешно он завершается только тогда, когда тестируемая функция выбрасывает исключение нужного типа.
Тест стал более надёжным, он больше не пропускает баги. А в блоке catch
можно проверить свойства пойманного исключения.
3. @Rule
Однако работать с конструкцией try-catch
неудобно.
Чтобы избавиться от неё, можно воспользоваться правилом ExpectedException
, входящим в стандартный дистрибутив JUnit 4:
Теперь код имеет простую плоскую структуру, хотя общее количество строк кода, к сожалению, увеличилось.
Но главная проблема этого способа заключается в том, что проверки в таком стиле выглядят противоестественно – сначала описывается поведение, а потом вызывается функция. Конечно, это дело вкуса, но мне нравится, когда проверки располагаются после вызова тестируемой функции.
4. AssertJ / catch-throwable
Более красивый способ, использующий возможности Java 8, предлагают дополнительные библиотеки, такие как AssertJ или catch-throwable. Вот пример работы с AssertJ:
Обращение к тестирумой функции оформлено в виде лямбда-выражения (анонимной функции), которое передаётся в “ловушку” для исключений catchThrowable
. Она перехватывает возникающее исключение и возвращает его как результат своей работы, давая возможность сохранить его в переменную и затем проверить его свойства. При этом проверки находятся после вызова тестируемой функции, читать код легче.
А если исключение не возникнет – “ловушка” сама выбросит исключение и тест упадёт.
5. JUnit 5
Но почему нужно использовать какие-то дополнительные библиотеки, почему тестовые фреймворки сами не предоставляют удобных возможностей для работы с ожидаемыми исключениями?
Уже предоставляют. Перехват исключений в JUnit 5 выглядит очень похоже на предыдущий пример:
Раньше такая возможность в JUnit отсутствовала, потому что предыдущие версии JUnit были ориентированы на более старые версии Java, где не было лямбда-выражений и написать подобный код было просто невозможно. Да, можно сделать нечто подобное с помощью анонимных классов, но это выглядит настолько ужасно, что конструкция try-catch
кажется верхом изящества.
Так что если вам приходится писать тесты, в которых проверяется возникновение исключений – есть повод присмотреться к новым возможностям JUnit 5.