Статьи

Правило JUnit ExpectedException: вне основ

Существуют разные способы обработки исключений в тестах 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 вы можете легко создавать надежный и повторно используемый код для ваших тестов исключений.