Существуют разные способы обработки исключений в тестах JUnit. Как я писал в одном из моих предыдущих постов , мой предпочтительный способ — использовать правило org.junit.rules.ExpectedException
. Как правило, правила используются в качестве альтернативы (или дополнения) для методов, аннотированных с помощью org.junit.Before
, org.junit.After
, org.junit.BeforeClass
или org.junit.AfterClass
, но они более мощные и более легко распределяется между проектами и классами. В этом посте я покажу более расширенное использование правила org.junit.rules.ExpectedException
.
Проверьте сообщение об исключении
Стандартная аннотация org.junit.Test
JUnit предлагает expected
атрибут, который позволяет указать Throwable
для Throwable
выполнения метода теста, если метод выбрасывает исключение из указанного класса. Во многих случаях этого кажется достаточно, но если вы хотите проверить сообщение об исключении — вы должны найти другой путь. С ExpectedException
довольно просто:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class ExpectedExceptionsTest { @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void verifiesTypeAndMessage() { thrown.expect(RuntimeException. class ); thrown.expectMessage( "Runtime exception occurred" ); throw new RuntimeException( "Runtime exception occurred" ); } } |
В приведенном выше фрагменте мы ожидаем, что сообщение contains
заданную подстроку. По сравнению только с проверкой типов это намного безопаснее. Почему? Давайте предположим, что у нас есть ExceptionThrower
который выглядит следующим образом:
1
2
3
4
5
6
7
8
|
class ExceptionsThrower { void throwRuntimeException( int i) { if (i <= 0 ) { throw new RuntimeException( "Illegal argument: i must be <= 0" ); } throw new RuntimeException( "Runtime exception occurred" ); } } |
Как вы можете видеть, оба исключения, создаваемые методом, являются RuntimeException
, поэтому в случае, если мы не проверяем сообщение, мы не уверены на 100%, какое исключение будет вызвано методом. Следующие тесты пройдут:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
@Test public void runtimeExceptionOccurs() { thrown.expect(RuntimeException. class ); // opposite to expected exceptionsThrower.throwRuntimeException( 0 ); } @Test public void illegalArgumentExceptionOccurs() { thrown.expect(RuntimeException. class ); // opposite to expected exceptionsThrower.throwRuntimeException( 1 ); } |
Проверка сообщения в исключении решит проблему и убедитесь, что вы проверяете искомое исключение. Это уже преимущество перед expected
атрибутом аннотированных методов @Test
.
Но что, если вам нужно проверить сообщение об исключении более сложным способом? ExpectedException
позволяет вам это, предоставив совпадение Hamcrest для expectMessage
(вместо String). Давайте посмотрим на пример ниже:
1
2
3
4
5
6
7
|
@Test public void verifiesMessageStartsWith() { thrown.expect(RuntimeException. class ); thrown.expectMessage(startsWith( "Illegal argument:" )); throw new RuntimeException( "Illegal argument: i must be <= 0" ); } |
Как вы можете ожидать, вы можете предоставить свои собственные средства сопоставления, чтобы проверить сообщение. Давайте посмотрим на пример.
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
|
@Test public void verifiesMessageMatchesPattern() { thrown.expect(RuntimeException. class ); thrown.expectMessage( new MatchesPattern( "[Ii]llegal .*" )); throw new RuntimeException( "Illegal argument: i must be <= 0" ); } class MatchesPattern extends TypeSafeMatcher<String> { private String pattern; public MatchesPattern(String pattern) { this .pattern = pattern; } @Override protected boolean matchesSafely(String item) { return item.matches(pattern); } @Override public void describeTo(Description description) { description.appendText( "matches pattern " ) .appendValue(pattern); } @Override protected void describeMismatchSafely(String item, Description mismatchDescription) { mismatchDescription.appendText( "does not match" ); } } |
Проверьте объект исключения
Соответствующих сообщений может быть недостаточно в определенных сценариях. У вас могут быть исключения с пользовательскими методами, и вы тоже хотите их проверить. Совершенно никаких проблем. ExpectedException
позволяет это с ExpectedException
методом, который принимает совпадения.
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
|
@Test public void verifiesCustomException() { thrown.expect(RuntimeException. class ); thrown.expect( new ExceptionCodeMatches( 1 )); throw new CustomException( 1 ); } class CustomException extends RuntimeException { private final int code; public CustomException( int code) { this .code = code; } public int getCode() { return code; } } class ExceptionCodeMatches extends TypeSafeMatcher<CustomException> { private int code; public ExceptionCodeMatches( int code) { this .code = code; } @Override protected boolean matchesSafely(CustomException item) { return item.getCode() == code; } @Override public void describeTo(Description description) { description.appendText( "expects code " ) .appendValue(code); } @Override protected void describeMismatchSafely(CustomException item, Description mismatchDescription) { mismatchDescription.appendText( "was " ) .appendValue(item.getCode()); } } |
Обратите внимание, что я реализовал методы TypeSafeMatcher
. Мне нужно, чтобы они выдавали красивые сообщения об ошибках, когда тест не пройден. Посмотрите на пример ниже:
1
2
3
|
java.lang.AssertionError: Expected: (an instance of java.lang.RuntimeException and expects code < 1 >) but: expects code < 1 > was < 2 > |
Проверка причины
Еще одна вещь, которую вы можете сделать с ExpectedException
— это проверить причину выданного исключения. Это также можно сделать с помощью пользовательских сопоставителей:
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
|
@Test public void verifiesCauseTypeAndAMessage() { thrown.expect(RuntimeException. class ); thrown.expectCause( new CauseMatcher(IllegalStateException. class , "Illegal state" )); throw new RuntimeException( "Runtime exception occurred" , new IllegalStateException( "Illegal state" )); } private static class CauseMatcher extends TypeSafeMatcher<Throwable> { private final Class<? extends Throwable> type; private final String expectedMessage; public CauseMatcher(Class<? extends Throwable> type, String expectedMessage) { this .type = type; this .expectedMessage = expectedMessage; } @Override protected boolean matchesSafely(Throwable item) { return item.getClass().isAssignableFrom(type) && item.getMessage().contains(expectedMessage); } @Override public void describeTo(Description description) { description.appendText( "expects type " ) .appendValue(type) .appendText( " and a message " ) .appendValue(expectedMessage); } } |
Резюме
Правило ExpectedException
— мощная функция. С добавлением средств Hamcrest
Hamcrest вы можете легко создавать надежный и повторно используемый код для ваших тестов исключений.
- Образцы кода можно найти на GitHub . Пожалуйста, проверьте мой предыдущий пост: 3 способа обработки исключений в JUnit. Какой выбрать?