Статьи

JUnit: тестирование исключения с помощью Java 8 и лямбда-выражений

В 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
@FunctionalInterface
public 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 и лямбда-выражений!

Ресурсы