В JUnit есть 3 популярных способа обработки исключений в вашем тестовом коде:
- попробуй поймай идиому
- С правилом JUnit
- С аннотацией
Какой из них мы должны использовать и когда?
попробуй поймай идиому
Эта идиома является одной из самых популярных, потому что она использовалась уже в JUnit 3.
01
02
03
04
05
06
07
08
09
10
11
|
@Test public void throwsExceptionWhenNegativeNumbersAreGiven() { try { calculator.add( "-1,-2,3" ); fail( "Should throw an exception if one or more of given numbers are negative" ); } catch (Exception e) { assertThat(e) .isInstanceOf(IllegalArgumentException. class ) .hasMessage( "negatives not allowed: [-1, -2]" ); } } |
Вышеуказанный подход является общей моделью. Тест не пройдёт, если не будет сгенерировано исключение, а само исключение будет проверено в предложении catch (в приведенном выше примере я использовал FEST Fluent Assertions), и хотя это совершенно нормально, я предпочитаю подход с
ExpectedException rule.
С правилом JUnit
Тот же пример можно создать, используя
Правило ExceptedException . Правило должно быть открытым полем, помеченным аннотацией @Rule. Обратите внимание, что правило «брошено» может быть повторно использовано во многих тестах.
01
02
03
04
05
06
07
08
09
10
11
|
@Rule public ExpectedException thrown = ExpectedException.none(); @Test public void throwsExceptionWhenNegativeNumbersAreGiven() { // arrange thrown.expect(IllegalArgumentException. class ); thrown.expectMessage(equalTo( "negatives not allowed: [-1, -2]" )); // act calculator.add( "-1,-2,3" ); } |
В целом, я нахожу приведенный выше код более читабельным, поэтому я использую этот подход в своих проектах.
Когда исключение не выдается, вы получите следующее сообщение: java.lang.AssertionError: Ожидаемый тест для выброса (экземпляр java.lang.IllegalArgumentException и исключение с сообщением «отрицания запрещены: [-1, -2]» ) Довольно мило
Но не все исключения я проверяю с помощью вышеуказанного подхода. Иногда мне нужно проверить только тип создаваемого исключения, а затем я использую аннотацию @Test.
С аннотацией
1
2
3
4
5
|
@Test (expected = IllegalArgumentException. class ) public void throwsExceptionWhenNegativeNumbersAreGiven() { // act calculator.add( "-1,-2,3" ); } |
Когда исключение не было выдано, вы получите следующее сообщение: java.lang.AssertionError: Ожидаемое исключение: java.lang.IllegalArgumentException
При таком подходе вы должны быть осторожны. Иногда бывает заманчиво ожидать общее исключение , RuntimeException или даже Throwable . И это считается плохой практикой, потому что ваш код может вызвать исключение в другом месте, чем вы ожидали, и ваш тест все равно пройдет!
Подводя итог, в своем коде я использую два подхода: с правилом JUnit и с аннотацией . Преимущества:
- Сообщения об ошибках, когда код не выдает исключение, обрабатываются автоматически
- Читаемость улучшена
- Там меньше кода для создания
А какие у тебя предпочтения?
Редактировать — 4-й способ
Я слышал о четвертом способе обработки исключения (один из моих коллег предложил это после прочтения этого поста) — использовать пользовательские аннотации.
На самом деле решение кажется на первый взгляд хорошим, но оно требует вашего собственного бегуна JUnit, поэтому у него есть недостаток: вы не можете использовать эту аннотацию, например, с бегуном Mockito.
Как практика кодирования я создал такую аннотацию, так что, возможно, кто-то найдет ее полезной
Использование
01
02
03
04
05
06
07
08
09
10
|
@RunWith (ExpectsExceptionRunner. class ) public class StringCalculatorTest { @Test @ExpectsException (type = IllegalArgumentException. class , message = "negatives not allowed: [-1]" ) public void throwsExceptionWhenNegativeNumbersAreGiven() throws Exception { // act calculator.add( "-1,-2,3" ); } } |
Вышеприведенный тест завершится неудачно с сообщением: java.lang.Exception: неожиданное сообщение об исключении, ожидается
но был
Аннотация
1
2
3
4
5
6
7
|
@Retention (RetentionPolicy.RUNTIME) @Target ({ElementType.METHOD}) public @interface ExpectsException { Class type(); String message() default "" ; } |
Бегун с некоторым кодом копирования и вставки
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
public class ExpectsExceptionRunner extends BlockJUnit4ClassRunner { public ExpectsExceptionRunner(Class klass) throws InitializationError { super (klass); } @Override protected Statement possiblyExpectingExceptions(FrameworkMethod method, Object test, Statement next) { ExpectsException annotation = method.getAnnotation(ExpectsException. class ); if (annotation == null ) { return next; } return new ExpectExceptionWithMessage(next, annotation.type(), annotation.message()); } class ExpectExceptionWithMessage extends Statement { private final Statement next; private final Class expected; private final String expectedMessage; public ExpectExceptionWithMessage(Statement next, Class expected, String expectedMessage) { this .next = next; this .expected = expected; this .expectedMessage = expectedMessage; } @Override public void evaluate() throws Exception { boolean complete = false ; try { next.evaluate(); complete = true ; } catch (AssumptionViolatedException e) { throw e; } catch (Throwable e) { if (!expected.isAssignableFrom(e.getClass())) { String message = "Unexpected exception, expected<" + expected.getName() + "> but was <" + e.getClass().getName() + ">" ; throw new Exception(message, e); } if (isNotNull(expectedMessage) && !expectedMessage.equals(e.getMessage())) { String message = "Unexpected exception message, expected<" + expectedMessage + "> but was<" + e.getMessage() + ">" ; throw new Exception(message, e); } } if (complete) { throw new AssertionError( "Expected exception: " + expected.getName()); } } private boolean isNotNull(String s) { return s != null && !s.isEmpty(); } } } |