JUnit 5 Expected Exception – assertThrows() Example
In JUnit 5, to write the test code that is expected to throw an exception, we should use Assertions.assertThrows(). The following test is expected to throw an exception of type ApplicationException or its subtype.
@Test void testExpectedException() < ApplicationException thrown = Assertions.assertThrows(ApplicationException.class, () ->< //Code under test >); Assertions.assertEquals("some message", thrown.getMessage()); >
Note that in JUnit 4, we needed to use @Test(expected = NullPointerException.class) syntax.
1. Assertions assertThrows() API
The assertThrows() method asserts that execution of the supplied executable block or lambda expression throws an exception of the expectedType . It is an overloaded method and takes the following parameters.
static T assertThrows(Class expectedType, Executable executable) static T assertThrows(Class expectedType, Executable executable, String message) static T assertThrows(Class expectedType, Executable executable, Supplier messageSupplier)
- expectedType – Test code is expected to throw an exception of this type.
- message – If the executable code does not throw any exception, this message will be printed along with the FAIL result.
- messageSupplier – The message will be retrieved from it in case the test fails.
1.2. Matching Exception Type
The assertThrows() will FAIL:
- Ifno exception is thrown from the executable block
- If an exception of a different type is thrown
The assertThrows() will PASS:
- If the code block throws an exception of the specified type or a subtype. For example, if we are expecting IllegalArgumentException and the test throws NumberFormatException then also the test will PASS because NumberFormatException extends IllegalArgumentException class.
Note that if we pass Exception.class as the expected exception type, any exception thrown from the executable block will make the assertion PASS since Exception is the super-type for all exceptions.
2. Demo – Expected Exception is Thrown
Given below is a very simple test that expects NumberFormatException to be thrown when the supplied code block is executed.
@Test void testExpectedException() < NumberFormatException thrown = Assertions.assertThrows(NumberFormatException.class, () ->< Integer.parseInt("One"); >, "NumberFormatException was expected"); Assertions.assertEquals("For input string: \"One\"", thrown.getMessage()); > @Test void testExpectedExceptionWithParentType() < Assertions.assertThrows(IllegalArgumentException.class, () ->< Integer.parseInt("One"); >); >
- In testExpectedException , The executable code is Integer.parseInt(«One») which throws NumberFormatException if method argument is not a valid numeric number. The assertThrows() the method expects – so this exception so the test result is PASS .
- In testExpectedExceptionWithParentType , we are executing the same code but this time we are excepting IllegalArgumentException which is the parent of NumberFormatException . This test also passes.
3. Demo – A Different Exception Type is Thrown, or No Exception
If the executable code throws any other exception type, then the test will FAIL . And even if the executable code does not throw any exception then also test will FAIL .
For example, in below example «1» is a valid number so no exception will be thrown. This test will fail with the message in the console.
@Test void testExpectedExceptionFail() < NumberFormatException thrown = Assertions .assertThrows(NumberFormatException.class, () ->< Integer.parseInt("1"); >, "NumberFormatException error was expected"); Assertions.assertEquals("Some expected message", thrown.getMessage()); >
In this post, we learned how to write a test that expects exceptions to be thrown. These tests are helpful in testing the code written in the catch blocks.
. как в JUnit проверять ожидаемые исключения?
Иногда возникновение исключения является ожидаемым поведением системы, и в тестах нужно проверять, что оно действительно возникает.
Ниже описаны пять способов, как в тестовом фреймворке JUnit перехватить ожидаемое исключение и проверить его свойства. Первые четыре из них можно использовать в JUnit 4, а последний способ использует новые возможности JUnit 5.
В качестве примера для демонстрации возьмём тест для функции стандартной библиотеки, создающей временный файл. Будем проверять, что при попытке создания файла в несуществующей директории возникает исключение типа IOException . При этом предварительно в том же самом тесте создаётся временная директория и тут же удаляется, так что мы получаем гарантированно несуществующую директорию, в которой и пытаемся создать файл:
import org.junit.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public class MyTest @Test public void testCreateTempFile() throws IOException Path tmpDir = Files.createTempDirectory("tmp"); tmpDir.toFile().delete(); Path tmpFile = Files.createTempFile(tmpDir, "test", ".txt"); > >
Разумеется, в таком виде тест упадёт, а в отчёте будет написано, что возникло исключение. А нам нужно, чтобы тест в этом случае наоборот помечался как успешный. Посмотрим, как это можно исправить.
1. @Test
Самый простой способ сообщить тестовому фреймворку о том, что ожидается исключение – указать дополнительный параметр expected в аннотации @Test :
import org.junit.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public class MyTest @Test(expected = IOException.class) public void testCreateTempFile() throws IOException Path tmpDir = Files.createTempDirectory("tmp"); tmpDir.toFile().delete(); Path tmpFile = Files.createTempFile(tmpDir, "test", ".txt"); > >
Этот параметр должен содержать тип ожидаемого исключения. Если возникнет исключение именно такого типа – тест пройдёт успешно. Если возникнет исключение другого типа или не возникнет вовсе – тест упадёт.
- Нельзя проверить текст сообщения или другие свойства возникшего исключения.
- Нельзя понять, где именно возникло исключение. В рассматриваемом примере оно могло быть выброшено не тестируемой функцией, а чуть раньше, при попытке создать временную директорию. Тест даже не смог добраться до вызова тестируемой функции – но при этом в отчёте он помечается как успешно пройденный!
Вторая из упомянутых проблем настолько ужасна, что я никому никогда не рекомендую использовать этот способ.
2. try-catch
Оба недостатка можно устранить, если перехватывать исключение явно при помощи конструкции try-catch :
import org.junit.Assert; import org.junit.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; public class MyTest @Test public void testCreateTempFile() throws IOException Path tmpDir = Files.createTempDirectory("tmp"); tmpDir.toFile().delete(); try Path tmpFile = Files.createTempFile(tmpDir, "test", ".txt"); Assert.fail("Expected IOException"); > catch (IOException thrown) Assert.assertNotEquals("", thrown.getMessage()); > // дальше идёт какой-то другой код // в нём тоже может появиться неожиданный IOException // если это случится -- тест упадёт > >
Если исключение возникает до блока try – тест падает, мы узнаём о том, что у него возникли проблемы.
Если тестируемая функция не выбрасывает вообще никакого исключения – мы попадаем на fail() в следующей строке, тест падает.
Если она выбрасывает исключение неподходящего типа – блок catch не ловит его, тест опять таки падает.
Успешно он завершается только тогда, когда тестируемая функция выбрасывает исключение нужного типа.
Тест стал более надёжным, он больше не пропускает баги. А в блоке catch можно проверить свойства пойманного исключения.
3. @Rule
Однако работать с конструкцией try-catch неудобно.
Чтобы избавиться от неё, можно воспользоваться правилом ExpectedException , входящим в стандартный дистрибутив JUnit 4:
import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; public class MyTest @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void testCreateTempFile() throws IOException Path tmpDir = Files.createTempDirectory("tmp"); tmpDir.toFile().delete(); thrown.expect(IOException.class); thrown.expectMessage(not(equalTo(""))); Path tmpFile = Files.createTempFile(tmpDir, "test", ".txt"); thrown = ExpectedException.none(); // дальше идёт какой-то другой код // в нём тоже может появиться неожиданный IOException // если это случится -- тест упадёт > >
Теперь код имеет простую плоскую структуру, хотя общее количество строк кода, к сожалению, увеличилось.
Но главная проблема этого способа заключается в том, что проверки в таком стиле выглядят противоестественно – сначала описывается поведение, а потом вызывается функция. Конечно, это дело вкуса, но мне нравится, когда проверки располагаются после вызова тестируемой функции.
4. AssertJ / catch-throwable
Более красивый способ, использующий возможности Java 8, предлагают дополнительные библиотеки, такие как AssertJ или catch-throwable. Вот пример работы с AssertJ:
import org.junit.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; public class MyTest @Test public void testCreateTempFile() throws IOException Path tmpDir = Files.createTempDirectory("tmp"); tmpDir.toFile().delete(); Throwable thrown = catchThrowable(() -> Files.createTempFile(tmpDir, "test", ".txt"); >); assertThat(thrown).isInstanceOf(IOException.class); assertThat(thrown.getMessage()).isNotBlank(); // дальше идёт какой-то другой код // в нём тоже может появиться неожиданный IOException // если это случится -- тест упадёт > >
Обращение к тестирумой функции оформлено в виде лямбда-выражения (анонимной функции), которое передаётся в “ловушку” для исключений catchThrowable . Она перехватывает возникающее исключение и возвращает его как результат своей работы, давая возможность сохранить его в переменную и затем проверить его свойства. При этом проверки находятся после вызова тестируемой функции, читать код легче.
А если исключение не возникнет – “ловушка” сама выбросит исключение и тест упадёт.
5. JUnit 5
Но почему нужно использовать какие-то дополнительные библиотеки, почему тестовые фреймворки сами не предоставляют удобных возможностей для работы с ожидаемыми исключениями?
Уже предоставляют. Перехват исключений в JUnit 5 выглядит очень похоже на предыдущий пример:
import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; public class MyTest @Test public void testCreateTempFile() throws IOException Path tmpDir = Files.createTempDirectory("tmp"); tmpDir.toFile().delete(); Throwable thrown = assertThrows(IOException.class, () -> Files.createTempFile(tmpDir, "test", ".txt"); >); assertNotNull(thrown.getMessage()); // дальше идёт какой-то другой код // в нём тоже может появиться неожиданный IOException // если это случится -- тест упадёт > >
Раньше такая возможность в JUnit отсутствовала, потому что предыдущие версии JUnit были ориентированы на более старые версии Java, где не было лямбда-выражений и написать подобный код было просто невозможно. Да, можно сделать нечто подобное с помощью анонимных классов, но это выглядит настолько ужасно, что конструкция try-catch кажется верхом изящества.
Так что если вам приходится писать тесты, в которых проверяется возникновение исключений – есть повод присмотреться к новым возможностям JUnit 5.
Автор: Алексей Баранцев
Если вам понравилась эта статья, вы можете поделиться ею в социальных сетях (кнопочки ниже), а потом вернуться на главную страницу блога и почитать другие мои статьи.
Ну а если вы не согласны с чем-то или хотите что-нибудь дополнить – оставьте комментарий ниже, может быть это послужит поводом для написания новой интересной статьи.