В JUnit есть много способов тестирования исключений в тестовом коде, включая try-catch idiom
, JUnit @Rule
, с библиотекой catch-exception
. Начиная с Java 8 у нас есть другой способ работы с исключениями: с лямбда-выражениями. В этом коротком посте в блоге я продемонстрирую простой пример того, как можно использовать возможности Java 8 и лямбда-выражений для тестирования исключений в JUnit.
Примечание. Мотивом для написания этого поста было сообщение, опубликованное на странице проекта catch-exception
:
Лямбда-выражения Java 8 сделают исключение catch-исключением. Поэтому этот проект больше не будет поддерживаться
SUT — тестируемая система
Мы протестируем исключения, выданные двумя нижеуказанными классами.
Первый:
01
02
03
04
05
06
07
08
09
10
|
class DummyService { public void someMethod() { throw new RuntimeException( "Runtime exception occurred" ); } public void someOtherMethod() { throw new RuntimeException( "Runtime exception occurred" , new IllegalStateException( "Illegal state" )); } } |
И второе:
1
2
3
4
5
6
7
8
9
|
class DummyService2 { public DummyService2() throws Exception { throw new Exception( "Constructor exception occurred" ); } public DummyService2( boolean dummyParam) throws Exception { throw new Exception( "Constructor exception occurred" ); } } |
Желаемый синтаксис
Моей целью было добиться синтаксиса, близкого к тому, который был у меня с библиотекой catch-exception
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
package com.github.kolorobot.exceptions.java8; import org.junit.Test; import static com.github.kolorobot.exceptions.java8.ThrowableAssertion.assertThrown; public class Java8ExceptionsTest { @Test public void verifiesTypeAndMessage() { assertThrown( new DummyService()::someMethod) // method reference // assertions .isInstanceOf(RuntimeException. class ) .hasMessage( "Runtime exception occurred" ) .hasNoCause(); } @Test public void verifiesCauseType() { assertThrown(() -> new DummyService().someOtherMethod( true )) // lambda expression // assertions .isInstanceOf(RuntimeException. class ) .hasMessage( "Runtime exception occurred" ) .hasCauseInstanceOf(IllegalStateException. class ); } @Test public void verifiesCheckedExceptionThrownByDefaultConstructor() { assertThrown(DummyService2:: new ) // constructor reference // assertions .isInstanceOf(Exception. class ) .hasMessage( "Constructor exception occurred" ); } @Test public void verifiesCheckedExceptionThrownConstructor() { assertThrown(() -> new DummyService2( true )) // lambda expression // assertions .isInstanceOf(Exception. class ) .hasMessage( "Constructor exception occurred" ); } @Test (expected = ExceptionNotThrownAssertionError. class ) // making test pass public void failsWhenNoExceptionIsThrown() { // expected exception not thrown assertThrown(() -> System.out.println()); } } |
Примечание. Преимущество перед catch-exception
заключается в том, что мы сможем тестировать конструкторы, которые генерируют исключения.
Создание «библиотеки»
Синтетический сахар
assertThrown
— это статический метод фабрики, создающий новый экземпляр ThrowableAssertion
со ссылкой на пойманное исключение.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
package com.github.kolorobot.exceptions.java8; public class ThrowableAssertion { public static ThrowableAssertion assertThrown(ExceptionThrower exceptionThrower) { try { exceptionThrower.throwException(); } catch (Throwable caught) { return new ThrowableAssertion(caught); } throw new ExceptionNotThrownAssertionError(); } // other methods omitted for now } |
ExceptionThrower
— это @FunctionalInterface
экземпляры которого можно создавать с помощью лямбда-выражений, ссылок на методы или ссылок на конструкторы. assertThrown
принимает ExceptionThrower
будет ожидать и быть готовым обработать исключение.
1
2
3
4
|
@FunctionalInterface public interface ExceptionThrower { void throwException() throws Throwable; } |
Утверждения
Чтобы закончить, нам нужно создать некоторые утверждения, чтобы мы могли проверить наши выражения в тестовом коде на предмет исключений в тесте. Фактически, ThrowableAssertion
— это своеобразное утверждение, предоставляющее нам возможность свободно проверять обнаруженное исключение. В приведенном ниже коде я использовал Hamcrest
Hamcrest для создания утверждений. Полный источник класса ThrowableAssertion
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package com.github.kolorobot.exceptions.java8; import org.hamcrest.Matchers; import org.junit.Assert; public class ThrowableAssertion { public static ThrowableAssertion assertThrown(ExceptionThrower exceptionThrower) { try { exceptionThrower.throwException(); } catch (Throwable caught) { return new ThrowableAssertion(caught); } throw new ExceptionNotThrownAssertionError(); } private final Throwable caught; public ThrowableAssertion(Throwable caught) { this .caught = caught; } public ThrowableAssertion isInstanceOf(Class<? extends Throwable> exceptionClass) { Assert.assertThat(caught, Matchers.isA((Class<Throwable>) exceptionClass)); return this ; } public ThrowableAssertion hasMessage(String expectedMessage) { Assert.assertThat(caught.getMessage(), Matchers.equalTo(expectedMessage)); return this ; } public ThrowableAssertion hasNoCause() { Assert.assertThat(caught.getCause(), Matchers.nullValue()); return this ; } public ThrowableAssertion hasCauseInstanceOf(Class<? extends Throwable> exceptionClass) { Assert.assertThat(caught.getCause(), Matchers.notNullValue()); Assert.assertThat(caught.getCause(), Matchers.isA((Class<Throwable>) exceptionClass)); return this ; } } |
Реализация AssertJ
Если вы используете библиотеку AssertJ
, вы можете легко создать версию ThrowableAssertion
используя org.assertj.core.api.ThrowableAssert
которая предоставляет множество полезных утверждений «из коробки». Реализация этого класса еще проще, чем с Hamcrest
представленным выше.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
package com.github.kolorobot.exceptions.java8; import org.assertj.core.api.Assertions; import org.assertj.core.api.ThrowableAssert; public class AssertJThrowableAssert { public static ThrowableAssert assertThrown(ExceptionThrower exceptionThrower) { try { exceptionThrower.throwException(); } catch (Throwable throwable) { return Assertions.assertThat(throwable); } throw new ExceptionNotThrownAssertionError(); } } |
Пример теста с AssertJ
:
01
02
03
04
05
06
07
08
09
10
11
12
|
public class AssertJJava8ExceptionsTest { @Test public void verifiesTypeAndMessage() { assertThrown( new DummyService()::someMethod) .isInstanceOf(RuntimeException. class ) .hasMessage( "Runtime exception occurred" ) .hasMessageStartingWith( "Runtime" ) .hasMessageEndingWith( "occurred" ) .hasMessageContaining( "exception" ) .hasNoCause(); } } |
Резюме
Всего за пару строк кода мы создали довольно крутой код, помогающий нам тестировать исключения в JUnit без какой-либо дополнительной библиотеки. И это было только начало. Используйте всю мощь Java 8 и лямбда-выражений!
Ресурсы
- Исходный код этой статьи доступен на GitHub (посмотрите пакет
com.github.kolorobot.exceptions.java8
) - Некоторые другие мои статьи о тестировании исключений в JUnit. Пожалуйста, посмотрите:
Ссылка: | JUnit: тестирование исключения с Java 8 и лямбда-выражениями от нашего партнера по JCG Рафаля Боровца в блоге Codeleak.pl . |