Исключения и время выполнения
Проверять только удачный сценарий мало: калькулятор должен отклонять нулевое количество и скидку 120%. Тесты для таких случаев вызывают код с плохими данными и ждут исключения. Для этого в JUnit есть специальные проверки — как и для того, чтобы убедиться, что код работает достаточно быстро.
assertThrows
assertThrows принимает класс ожидаемого исключения и лямбду с кодом. Тест проходит, если лямбда выбросила исключение этого класса или его наследника:
// src/test/java/com/example/shop/PriceCalculatorTest.java
@Test
void zeroQuantityIsRejected() {
PriceCalculator calculator = new PriceCalculator();
assertThrows(IllegalArgumentException.class,
() -> calculator.total(new BigDecimal("12.00"), 0));
}Лямбда нужна, чтобы JUnit сам вызвал код внутри своего try. Без неё исключение вылетело бы ещё до того, как assertThrows начал работать.
Проверка сообщения
assertThrows возвращает пойманное исключение — можно проверить его сообщение или поля:
// src/test/java/com/example/shop/PriceCalculatorTest.java
@Test
void exceptionExplainsTheProblem() {
PriceCalculator calculator = new PriceCalculator();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> calculator.applyDiscount(new BigDecimal("50.00"), 120));
assertEquals("Скидка должна быть от 0 до 100: 120", e.getMessage());
}Если нужен именно этот класс, а не наследник, используйте assertThrowsExactly.
Исключения быть не должно
Скидка 100% допустима. assertDoesNotThrow фиксирует это и возвращает результат лямбды:
// src/test/java/com/example/shop/PriceCalculatorTest.java
@Test
void fullDiscountIsAllowed() {
PriceCalculator calculator = new PriceCalculator();
BigDecimal result = assertDoesNotThrow(
() -> calculator.applyDiscount(new BigDecimal("50.00"), 100));
assertEquals(new BigDecimal("0.00"), result);
}Если код всё же выбросит исключение, тест упадёт с понятным сообщением, а не с ошибкой и стеком вызовов.
Ограничение по времени
assertTimeout роняет тест, если код работает дольше заданного Duration:
// src/test/java/com/example/shop/PriceCalculatorTest.java
@Test
void totalIsFast() {
PriceCalculator calculator = new PriceCalculator();
BigDecimal total = assertTimeout(Duration.ofMillis(100),
() -> calculator.total(new BigDecimal("0.99"), 1_000_000));
assertEquals(new BigDecimal("990000.00"), total);
}| Проверка | Что происходит, когда время вышло |
|---|---|
assertTimeout | Ждёт окончания кода, затем сообщает execution exceeded timeout of 50 ms by 151 ms |
assertTimeoutPreemptively | Прерывает код в другом потоке: execution timed out after 50 ms |
Прерывающий вариант быстрее при падении, но выполняет код в отдельном потоке — это может сломать код, который опирается на текущий поток, например транзакции. Для всего тестового метода есть ещё аннотация @Timeout(5) (секунды).
Частая ошибка
assertThrows падает не только когда исключение другого типа, но и когда его нет вовсе. Оба сообщения стоит узнавать:
org.opentest4j.AssertionFailedError: Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.
org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: <java.lang.IllegalStateException> but was: <java.lang.IllegalArgumentException>
...
Caused by: java.lang.IllegalArgumentException: Количество должно быть больше нуля: 0Первое появилось потому, что скидка 100% допустима и код ничего не выбросил. Второе — потому что тест ждал не тот класс: PriceCalculator выбрасывает IllegalArgumentException. В обоих случаях ошибся тест, а не код — прочитайте сообщение, прежде чем править PriceCalculator.