Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Исключения и время выполнения

Проверять только удачный сценарий мало: калькулятор должен отклонять нулевое количество и скидку 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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.