Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Exceptions and timeouts

Checking only the happy path is not enough: the calculator must reject a zero quantity and a 120% discount. The tests for such cases call the code with bad data and expect an exception. JUnit has special assertions for this, as well as for checking that code runs fast enough.

assertThrows

assertThrows takes the expected exception class and a lambda with the code. The test passes if the lambda throws an exception of this class or its subclass:

// 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));
}

The lambda is needed so that JUnit calls the code itself, inside its own try. Without it, the exception would fly out before assertThrows even starts.

Checking the message

assertThrows returns the caught exception — you can check its message or fields:

// 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("Discount must be between 0 and 100: 120", e.getMessage());
}

If you need the exact class and not a subclass, use assertThrowsExactly.

No exception expected

A 100% discount is allowed. assertDoesNotThrow documents this and returns the result of the lambda:

// 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);
}

If the code throws anyway, the test fails with a clear message instead of an error with a stack trace.

Timeouts

assertTimeout fails the test if the code runs longer than the given 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);
}
AssertionBehavior when time runs out
assertTimeoutWaits for the code to finish, then reports execution exceeded timeout of 50 ms by 151 ms
assertTimeoutPreemptivelyInterrupts the code in another thread: execution timed out after 50 ms

The preemptive version is faster on failure, but runs the code in a separate thread — this can break code that relies on the current thread, such as transactions. For a whole test method there is also the @Timeout(5) annotation (seconds).

Common mistake

assertThrows fails not only when the exception is of a different type, but also when there is none at all. Both messages are worth recognizing:

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: Quantity must be positive: 0

The first one appeared because a 100% discount is valid and the code did not throw. The second because the test waited for the wrong class: PriceCalculator throws IllegalArgumentException. In both cases the test was wrong, not the code — read the message before changing PriceCalculator.

Thanks for reading!

Donate to the site — we work hard for you.