Ловим ошибки правильно: как тестировать исключения в JUnit в 2025

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: «Почему мой тест падает, когда должен проходить?»

Вы когда-нибудь писали юнит-тест, который должен был ловить исключение, а он вместо этого падал с красной ошибкой? Знакомо? Вопрос «How do you assert that a certain exception is thrown in JUnit tests?» на Stack Overflow собрал тысячи просмотров и сотни комментариев. И это не случайно: тестирование исключений — одна из тех тем, где даже опытные разработчики иногда спотыкаются.

В 2025 году, когда качество кода стало критическим фактором (особенно в микросервисах и AI-пайплайнах), умение правильно проверять исключения — это не просто «хороший тон», а базовая компетенция. В этой статье мы разберём все актуальные способы assert исключений в JUnit 5, покажем подводные камни и дадим чек-лист лучших практик. Поехали!

Почему важно тестировать исключения?

Представьте: вы пишете метод divide(int a, int b). Если b == 0, метод должен кидать ArithmeticException. Без теста на это поведение вы рискуете, что через месяц другой разработчик (или вы сами) случайно уберёт проверку, и приложение упадёт в production с непонятной ошибкой.

Тесты на исключения — это ваша страховка. Они документируют ожидаемое поведение системы в граничных случаях. И JUnit 5 даёт для этого несколько элегантных инструментов.

Способ 1: assertThrows — король современного тестирования

Начиная с JUnit 5 (и его пакета org.junit.jupiter.api), метод assertThrows стал стандартом де-факто. Почему? Потому что он лаконичный, читаемый и функциональный.

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

@Test
void whenDivideByZero_thenArithmeticException() {
    assertThrows(ArithmeticException.class, 
        () -> calculator.divide(10, 0));
}

Что здесь происходит? assertThrows принимает класс ожидаемого исключения и лямбду с кодом, который должен его выбросить. Если исключение выбрасывается — тест зелёный. Если нет — красный. Всё просто и наглядно.

Расширенный вариант: проверяем сообщение исключения

Иногда недостаточно просто проверить тип исключения — нужно убедиться, что сообщение содержит правильный текст. assertThrows возвращает объект исключения, и вы можете проверить его свойства:

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

@Test
void whenInvalidEmail_thenExceptionWithMessage() {
    IllegalArgumentException exception = assertThrows(
        IllegalArgumentException.class,
        () -> userService.register("invalid-email")
    );
    
    assertTrue(exception.getMessage().contains("Некорректный email"));
}

Этот подход особенно полезен при тестировании кастомных исключений с полями (например, кодом ошибки).

Способ 2: Старый добрый expected (JUnit 4 legacy)

Если вы работаете с JUnit 4 или поддерживаете легаси-код, то наверняка встречали атрибут expected в аннотации @Test:

@Test(expected = ArithmeticException.class)
public void whenDivideByZero_thenException() {
    calculator.divide(10, 0);
}

Минус: этот способ не позволяет проверить сообщение исключения или его свойства. Кроме того, если исключение выбросится раньше, чем вы ожидаете (например, в строчке инициализации), тест может пройти ложно. В 2025 году рекомендуем избегать этого подхода в новом коде.

Способ 3: Try-catch с флагом (для фанатов контроля)

До появления JUnit 5 многие использовали конструкцию с try-catch и булевым флагом. Выглядит громоздко, но даёт полный контроль:

@Test
void whenDivideByZero_thenExceptionCaught() {
    boolean thrown = false;
    try {
        calculator.divide(10, 0);
    } catch (ArithmeticException e) {
        thrown = true;
        assertEquals("Division by zero", e.getMessage());
    }
    assertTrue(thrown, "ArithmeticException was not thrown");
}

Сегодня этот способ считается устаревшим. Он многословен, легко допустить ошибку (например, забыть установить флаг). Используйте его только если у вас нет выбора (например, в очень старых версиях JUnit).

Способ 4: Правило ExpectedException (JUnit 4 Rule)

Ещё один пережиток прошлого — @Rule ExpectedException. Он позволял декларативно указать ожидаемое исключение и проверять его свойства:

import org.junit.Rule;
import org.junit.rules.ExpectedException;

public class CalculatorTest {
    @Rule
    public ExpectedException thrown = ExpectedException.none();
    
    @Test
    public void whenDivideByZero_thenException() {
        thrown.expect(ArithmeticException.class);
        thrown.expectMessage("Division by zero");
        calculator.divide(10, 0);
    }
}

В JUnit 5 этот механизм удалён. Если вы мигрируете с JUnit 4, просто замените его на assertThrows — это будет чище и надёжнее.

Лучшие практики 2025: чек-лист для профессионала

Мы собрали рекомендации, которые помогут вам писать тесты на исключения, от которых не будет болеть голова у коллег:

  • Всегда используйте assertThrows из JUnit 5. Это современно, читаемо и функционально.
  • Проверяйте сообщение исключения, если оно несёт бизнес-смысл. Не проверяйте точное совпадение — используйте contains() или matches(), чтобы тест не сломался при смене формулировки.
  • Тестируйте только одно исключение на тест. Если метод может кидать разные исключения при разных условиях — пишите отдельные тесты. Это улучшает читаемость и упрощает отладку.
  • Не тестируйте unchecked исключения без необходимости. NullPointerException или ArrayIndexOutOfBoundsException обычно говорят о баге в коде, а не о контракте метода. Лучше исправить баг, чем тестировать его.
  • Документируйте причину исключения в assertThrows. Добавляйте сообщение в третий параметр: assertThrows(..., () -> ..., "Должно быть исключение при делении на 0"). Это помогает быстро понять, что пошло не так, если тест упадёт.
  • Используйте вложенные assertThrows для сложной логики. Если ваш метод делает несколько операций, и вы хотите проверить, что исключение выбрасывается на конкретном шаге — оберните именно этот шаг в лямбду.

Реальный пример: тестирование сервиса с валидацией

Давайте соберём всё вместе. Предположим, у нас есть сервис регистрации пользователей, который кидает RegistrationException с кодом ошиб

Блоги

Рекомендаций по книгам