Как проверять исключения в тестах JUnit: Лучшие практики для проверки исключений
Как можно использовать JUnit идиоматически для проверки, что некоторый код выбрасывает исключение?
Хотя я могу, конечно, сделать что-то вроде этого:
@Test
public void testFooThrowsIndexOutOfBoundsException() {
boolean thrown = false;
try {
foo.doStuff();
} catch (IndexOutOfBoundsException e) {
thrown = true;
}
assertTrue(thrown);
}
Я помню, что есть аннотация или Assert.xyz или что-то, что гораздо менее громоздкое и гораздо больше в духе JUnit для таких ситуаций.
Самый идиоматичный способ тестирования исключений в современном JUnit (JUnit 5) — использование метода assertThrows(), который обеспечивает чистый и читаемый синтаксис, четко выражающий намерение вашего теста. Для JUnit 4 вы бы использовали аннотацию @Test(expected = Exception.class), хотя подход JUnit 5 обычно предпочтительнее благодаря своей гибкости и лучшей отчетности об ошибках.
Содержание
- Современный подход JUnit 5 с использованием assertThrows()
- Устаревшие методы JUnit 4
- Расширенные шаблоны тестирования исключений
- Лучшие практики тестирования исключений
- Распространенные ошибки и решения
- Сравнение подходов к тестированию исключений
Современный подход JUnit 5 с использованием assertThrows()
Метод assertThrows() в JUnit 5 — это рекомендуемый подход для тестирования исключений. Он более выразительный, предоставляет лучшие сообщения об ошибках и позволяет выполнять дополнительные проверки для брошенного исключения.
Базовое использование
import static org.junit.jupiter.api.Assertions.*;
@Test
public void testFooThrowsIndexOutOfBoundsException() {
// Лямбда-выражение, содержащее код, который должен выбросить исключение
IndexOutOfBoundsException exception = assertThrows(
IndexOutOfBoundsException.class,
() -> foo.doStuff()
);
// Опционально: дополнительные проверки для исключения
assertEquals("Index out of bounds", exception.getMessage());
}
Ключевые преимущества
- Чистый синтаксис: лямбда-выражение делает понятным, какой код тестируется
- Лучшие сообщения об ошибках: когда тест не проходит, JUnit предоставляет подробную информацию о том, что ожидалось и что произошло на самом деле
- Доступ к объекту исключения: вы можете выполнять дополнительные проверки для брошенного исключения
- Проверка типов: проверка типов исключений на этапе компиляции
Тестирование нескольких операторов
Для тестирования нескольких операторов, которые должны выбросить исключение:
@Test
public void testComplexOperationThrowsException() {
assertThrows(
IllegalArgumentException.class,
() -> {
int result = complexOperation(a, b);
validateResult(result);
}
);
}
Устаревшие методы JUnit 4
Если вы все еще используете JUnit 4, у вас есть несколько вариантов, хотя они менее гибки, чем подход JUnit 5.
Аннотация @Test(expected)
@Test(expected = IndexOutOfBoundsException.class)
public void testFooThrowsIndexOutOfBoundsExceptionJUnit4() {
foo.doStuff();
}
Ограничения:
- Нет доступа к объекту исключения для дополнительных проверок
- Менее понятно, какой конкретный код тестируется
- Может вводить в заблуждение, если несколько операторов могут выбросить исключение
Правило ExpectedException
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testFooThrowsIndexOutOfBoundsExceptionWithRule() {
thrown.expect(IndexOutOfBoundsException.class);
thrown.expectMessage("Index out of bounds");
foo.doStuff();
}
Преимущества перед @Test(expected):
- Можно указать ожидаемое сообщение исключения
- Более гибкая конфигурация
- Более четкое выражение намерения теста
Расширенные шаблоны тестирования исключений
Тестирование сообщений и деталей исключений
@Test
public void testExceptionDetails() {
InvalidDataException exception = assertThrows(
InvalidDataException.class,
() -> validator.validate(data)
);
// Проверка сообщения исключения
assertEquals("Invalid email format", exception.getMessage());
// Проверка свойств исключения
assertNotNull(exception.getErrorCode());
assertEquals("EMAIL_FORMAT_ERROR", exception.getErrorCode());
// Проверка причины исключения
assertInstanceOf(ParseException.class, exception.getCause());
}
Тестирование нескольких типов исключений
@Test
public void testMultiplePossibleExceptions() {
Exception exception = assertThrows(Exception.class, () -> {
// Код, который может выбросить разные типы исключений
riskyOperation();
});
// Затем проверка конкретного типа
assertInstanceOf(IllegalArgumentException.class, exception);
}
Тестирование с помощью пользовательских вспомогательных методов для проверок
@Test
public void testWithCustomHelper() {
// Вспомогательный метод, который объединяет тестирование исключений с другими проверками
assertThrowsWithMessage(
IndexOutOfBoundsException.class,
"Index must be between 0 and 9",
() -> list.get(15)
);
}
Лучшие практики тестирования исключений
1. Будьте конкретны в типах исключений
// Хорошо - конкретный тип исключения
assertThrows(NoSuchElementException.class, () -> iterator.next());
// Плохо - слишком общий
assertThrows(RuntimeException.class, () -> iterator.next());
2. Тестируйте сообщения исключений, когда это уместно
@Test
public void testInvalidArgumentMessage() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> calculator.divide(10, 0)
);
assertEquals("Division by zero is not allowed", exception.getMessage());
}
3. Тестируйте причины исключений, когда это уместно
@Test
public void testExceptionCause() {
DataProcessingException exception = assertThrows(
DataProcessingException.class,
() -> processor.process(data)
);
assertInstanceOf(IOException.class, exception.getCause());
}
4. Не переусердствуйте с тестированием исключений
Тестируйте только те исключения, которые являются частью вашего контракта или API. Не тестируйте внутренние детали реализации.
// Тестирование контракта публичного API
assertThrows(IllegalStateException.class, () -> service.start());
// Не тестируйте внутренние детали реализации
// assertThrows(SQLException.class, () -> service.internalMethod());
5. Используйте параметризованные тесты для сценариев с исключениями
@ParameterizedTest
@ValueSource(strings = {"", " ", "null"})
public void testEmptyInputThrowsException(String input) {
assertThrows(IllegalArgumentException.class, () -> parser.parse(input));
}
Распространенные ошибки и решения
Ошибка 1: Тестирование неправильного кода
// Проблема: тестирование и настройки, и операции
assertThrows(RuntimeException.class, () -> {
setup(); // Это может выбросить исключение
operation(); // Это то, что мы на самом деле хотим протестировать
});
// Решение: тестирование только релевантного кода
assertThrows(RuntimeException.class, operation::perform);
Ошибка 2: Не тестирование деталей исключений
// Проблема: только проверка, что исключение выброшено
assertThrows(Exception.class, () -> method());
// Решение: тестирование конкретных деталей исключения
SpecificException exception = assertThrows(
SpecificException.class,
() -> method()
);
assertEquals("Expected message", exception.getMessage());
Ошибка 3: Тестирование деталей реализации
// Проблема: тестирование внутреннего поведения
assertThrows(SQLException.class, () -> dao.internalQuery());
// Решение: тестирование контракта API
assertThrows(DataAccessException.class, () -> dao.findById(-1));
Сравнение подходов к тестированию исключений
| Подход | Читаемость | Гибкость | Сообщения об ошибках | Доступ к исключению |
|---|---|---|---|---|
assertThrows() (JUnit 5) |
Отличная | Высокая | Подробные | Полный доступ |
@Test(expected) (JUnit 4) |
Хорошая | Низкая | Базовые | Нет |
Правило ExpectedException |
Хорошая | Средняя | Хорошая | Частичный доступ |
| Ручной подход try-catch | Плохая | Высокая | Ручной | Полный доступ |
Когда использовать каждый подход
Используйте assertThrows(), когда:
- Вы используете JUnit 5
- Вы хотите получить наиболее читаемые и поддерживаемые тесты
- Вам нужен доступ к объекту исключения для дополнительных проверок
Используйте @Test(expected), когда:
- Вы застряли с JUnit 4
- Вам нужно только проверить, что исключение выброшено
- Вы не можете сразу перейти на JUnit 5
Используйте ручной подход try-catch, когда:
- Вам нужна сложная логика обработки исключений
- Вы работаете с устаревшим кодом, который сложно рефакторить
- Вам нужно протестировать несколько сценариев исключений в одном тесте
Заключение
JUnit предоставляет несколько отличных вариантов для тестирования исключений, при этом assertThrows() в JUnit 5 является современным стандартом. При написании тестов для исключений сосредоточьтесь на тестировании контрактов вашего API, а не деталей реализации, будьте конкретны в типах исключений и используйте дополнительные проверки, когда нужно тестировать сообщения или причины исключений.
Для новых проектов всегда предпочитайте метод assertThrows() в JUnit 5 благодаря его превосходной читаемости, гибкости и отчетности об ошибках. Если вы поддерживаете код на JUnit 4, аннотация @Test(expected) приемлема для простых случаев, но рассмотрите возможность перехода на JUnit 5 для получения лучших возможностей тестирования.
Помните, что хорошее тестирование исключений не только проверяет, что ваш код правильно обрабатывает ошибки, но также служит документацией для поведения вашего API в случае ошибок, что облегчает другим разработчикам понимание того, как правильно использовать ваш код.