В 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
|
@FunctionalInterfacepublic 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 . |