В 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(); } }} |