JUnit 5: Как утверждать, исключение брошено?

Есть ли лучший способ утверждать, что метод вызывает исключение в JUnit 5?

В настоящее время я должен использовать @Rule, чтобы проверить, что мой тест выдает исключение, но это не работает в тех случаях, когда я ожидаю, что несколько методов выдают исключения в моем тесте.

14 ответов

Решение

Ты можешь использовать assertThrows(), что позволяет тестировать несколько исключений в рамках одного теста. Благодаря поддержке лямбд в Java 8 это, вероятно, станет каноническим способом проверки исключений в JUnit.

Согласно документам JUnit:

import static org.junit.jupiter.api.Assertions.assertThrows;

...

@Test
void exceptionTesting() {
    Executable closureContainingCodeToTest = () -> throw new IllegalArgumentException("a message");

    assertThrows(IllegalArgumentException.class, closureContainingCodeToTest, "a message");
}

В Java 8 и JUnit 5 (Юпитер) мы можем утверждать исключения следующим образом. С помощью org.junit.jupiter.api.Assertions.assertThrows

public static T assertThrows(класс Ожидаемый тип, исполняемый файл)

Утверждает, что выполнение предоставленного исполняемого файла вызывает исключение ожидаемого типа и возвращает исключение.

Если не сгенерировано исключение или если сгенерировано исключение другого типа, этот метод завершится ошибкой.

Если вы не хотите выполнять дополнительные проверки экземпляра исключения, просто проигнорируйте возвращаемое значение.

@Test
public void itShouldThrowNullPointerExceptionWhenBlahBlah() {
    assertThrows(NullPointerException.class,
            ()->{
            //do whatever you want to do here
            //ex : objectName.thisMethodShoulThrowNullPointerExceptionForNullParameter(null);
            });
}

Этот подход будет использовать функциональный интерфейс Executable в org.junit.jupiter.api,

См.:

Они изменили его в JUnit 5 (ожидается: InvalidArgumentException, фактический: вызванный метод), и код выглядит так:

@Test
public void wrongInput() {
    Throwable exception = assertThrows(InvalidArgumentException.class,
            ()->{objectName.yourMethod("WRONG");} );
}

Вы можете использовать assertThrows(), Но с assertThrows ваше утверждение пройдет, даже если выброшенное исключение имеет дочерний тип.

Это потому, что JUnit 5 проверяет тип исключения, вызывая Class.isIntance(..), Class.isInstance(..) вернет истину, даже если выброшенное исключение относится к дочерним типам.

Обходной путь для этого - утверждение класса:

      Throwable throwable =  assertThrows(Throwable.class, () -> {
    service.readFile("sampleFile.txt");
});
assertEquals(FileNotFoundException.class, throwable.getClass());

Теперь Junit5 предоставляет возможность утверждать исключения

Вы можете протестировать как общие исключения, так и настраиваемые исключения

Общий сценарий исключения:

ExpectGeneralException.java

public void validateParameters(Integer param ) {
    if (param == null) {
        throw new NullPointerException("Null parameters are not allowed");
    }
}

ExpectGeneralExceptionTest.java

@Test
@DisplayName("Test assert NullPointerException")
void testGeneralException(TestInfo testInfo) {
    final ExpectGeneralException generalEx = new ExpectGeneralException();

     NullPointerException exception = assertThrows(NullPointerException.class, () -> {
            generalEx.validateParameters(null);
        });
    assertEquals("Null parameters are not allowed", exception.getMessage());
}

Вы можете найти образец для тестирования CustomException здесь: пример кода исключения assert

ExpectCustomException.java

public String constructErrorMessage(String... args) throws InvalidParameterCountException {
    if(args.length!=3) {
        throw new InvalidParameterCountException("Invalid parametercount: expected=3, passed="+args.length);
    }else {
        String message = "";
        for(String arg: args) {
            message += arg;
        }
        return message;
    }
}

ExpectCustomExceptionTest.java

@Test
@DisplayName("Test assert exception")
void testCustomException(TestInfo testInfo) {
    final ExpectCustomException expectEx = new ExpectCustomException();

     InvalidParameterCountException exception = assertThrows(InvalidParameterCountException.class, () -> {
            expectEx.constructErrorMessage("sample ","error");
        });
    assertEquals("Invalid parametercount: expected=3, passed=2", exception.getMessage());
}

Ты можешь использовать assertThrows(), Мой пример взят из документов http://junit.org/junit5/docs/current/user-guide/

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

....

@Test
void exceptionTesting() {
    Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
        throw new IllegalArgumentException("a message");
    });
    assertEquals("a message", exception.getMessage());
}

Я думаю, что это еще более простой пример

List<String> emptyList = new ArrayList<>();
Optional<String> opt2 = emptyList.stream().findFirst();
assertThrows(NoSuchElementException.class, () -> opt2.get());

призвание get() на необязательный, содержащий пустой ArrayList бросит NoSuchElementException, assertThrows объявляет ожидаемое исключение и предоставляет лямбда-поставщика (не принимает аргументов и возвращает значение).

Спасибо @prime за его ответ, который я надеюсь развить.

Еще проще one liner. В этом примере с использованием Java 8 и JUnit 5 лямбда-выражения или фигурные скобки не требуются.

import static org.junit.jupiter.api.Assertions.assertThrows;

@Test
void exceptionTesting() {

    assertThrows(MyException.class, myStackObject::doStackAction, "custom message if assertion fails..."); 

// note, no parenthesis on doStackAction ex ::pop NOT ::pop()
}

Мое решение:

          protected <T extends Throwable> void assertExpectedException(ThrowingRunnable methodExpectedToFail, Class<T> expectedThrowableClass,
        String expectedMessage) {
    T exception = assertThrows(expectedThrowableClass, methodExpectedToFail);
    assertEquals(expectedMessage, exception.getMessage());
}

И вы можете назвать это так:

          assertExpectedException(() -> {
        carService.findById(id);
    }, IllegalArgumentException.class, "invalid id");
      This is what I do when testing to make sure an exception has been thrown

    
    //when
    final var tripConsumer = new BusTripConsumer(inputStream);
    final Executable executable = () -> tripConsumer.deserialiseTripData();

    //then
    assertThrows(IllegalArgumentException.class, executable);

На самом деле я думаю, что есть ошибка в документации для этого конкретного примера. Метод, который предназначен, является ожидаемым

public static void assertThrows(
public static <T extends Throwable> T expectThrows(

Хорошая практика кодирования - генерировать исключения, когда данные недействительны. Лучший тестовый пример - проверить, было ли выбрано исключение для недопустимого аргумента. Для тестирования обоих сценариев JUnit 5 предоставляет нам методы assertThrows и assertDoesNotThrow .

пример кода отсюда

      public class AppleCalculator {
public String getError(String apple) {
    if (Objects.isNull(apple)) {
        throw new NullPointerException("apple cannot be null");
    }
    return apple;
}

}

       @Test
void assertThrowTest() {
    AppleCalculator appleCalculator = new AppleCalculator();
    assertThrows(NullPointerException.class, () -> appleCalculator.getError(null));
    assertThrows(NullPointerException.class, () -> appleCalculator.getError(null), "it will throw exception");
    Supplier<String> messageSupplier = () -> "it will throw exception";
    assertThrows(NullPointerException.class, () -> appleCalculator.getError(null), messageSupplier);
    Exception exception = assertThrows(NullPointerException.class, () -> appleCalculator.getError(null), messageSupplier);
    assertEquals("apple cannot be null", exception.getMessage());
}

@Test
void assertDoesNotThrowTest() {
    AppleCalculator appleCalculator = new AppleCalculator();
    assertDoesNotThrow(() -> appleCalculator.getError("apple"));
    assertDoesNotThrow(() -> appleCalculator.getError("apple"), "it will not throw exception");
    Supplier<String> messageSupplier = () -> "it will not throw exception";
    String response = assertDoesNotThrow(() -> appleCalculator.getError("apple"), messageSupplier);
    assertEquals("apple", response);
}

Исключение, объявленное в assertThrows (ExceptionType.class,..), должно быть сгенерировано, если выброшенное исключение отличается или исключение не сгенерировано вообще, тогда тестовый пример завершится ошибкой.

  • если assertDoesNotThrow обнаружит исключение, тестовый пример завершится ошибкой.
  • Исключение, объявленное в assertThrows (ExceptionType.class,..), будет включать все его дочернее исключение, т.е. если объявлено родительское исключение и тестовый пример вызывает дочернее исключение, тестовый пример все равно будет считаться пройденным.

Есть 3 способа заявить об определенном исключении в Junit. Давайте напишем кейсы для него.

1. идиома try-catch Эта идиома является одной из самых популярных, потому что она использовалась уже в JUnit 3. Этот подход является распространенным паттерном. Тест завершится неудачей, если не сгенерировано исключение, а само исключение проверено в предложении catch.

@Test
public void convertIntoUpperCase_withInvalidInput_tryCatchIdiom() {
    try {
        exceptionHandling.convertIntoUpperCase("");
        fail("It should throw IllegalArgumentException");
    } catch (IllegalArgumentException e) {
        Assertions.assertThat(e)
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("Empty value is passed.");
    }
}

2. Ожидаемая аннотация @Test В этом подходе мы указываем ожидаемое исключение в @Test, как показано ниже @Test(Ожидаемый = IllegalArgumentException.class)

Когда исключение не было выдано, вы получите следующее сообщение: java.lang.AssertionError: Ожидаемое исключение: java.lang.IllegalArgumentException

При таком подходе вы должны быть осторожны. Иногда бывает заманчиво ожидать общее исключение, RuntimeException или даже Throwable. И это считается плохой практикой, потому что ваш код может вызвать исключение в других местах, чем вы ожидали, и ваш тест все равно пройдет!

Одним из недостатков этого подхода является то, что вы не можете утверждать сообщение об исключении.

@Test(expected = IllegalArgumentException.class)
public void convertIntoUpperCase_withInvalidInput_testExpected() {
    exceptionHandling.convertIntoUpperCase("");
}

3. Junit @Rule Тот же пример может быть создан с использованием правила ExceptedException. Правило должно быть открытым полем, помеченным аннотацией @Rule.

    @Test
    public void convertIntoUpperCase_withInvalidInput_ExpectedExceptionRule() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Empty value is passed.");
        exceptionHandling.convertIntoUpperCase("");
    }

Я нахожу приведенный выше код более читабельным, поэтому я предпочитаю использовать этот подход.

Когда исключение не выдается, вы получите следующее сообщение: java.lang.AssertionError: Ожидаемый тест для выброса (экземпляр java.lang.IllegalArgumentException и исключение с сообщением "Пустое значение передано".). Довольно мило

Но не все исключения я проверяю с помощью вышеуказанного подхода. Иногда мне нужно проверить только тип создаваемого исключения, а затем я использую аннотацию @Test.

Вот простой способ.

@Test
void exceptionTest() {

   try{
        model.someMethod("invalidInput");
        fail("Exception Expected!");
   }
   catch(SpecificException e){

        assertTrue(true);
   }
   catch(Exception e){
        fail("wrong exception thrown");
   }

}

Успешно, только когда вы ожидаете исключения.

Другие вопросы по тегам