Stage 7: Tools and testing, lesson 3 of 7

Unit testing with JUnit 5

Intermediate3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

JUnit 5 (JUnit Jupiter) is the standard testing framework.

  • Mark tests with @Test and name them by behaviour: appliesFestiveDiscount().
  • Follow Arrange, Act, Assert: set up, call the code, check the result.
  • Assert with assertEquals, assertTrue and assertThrows, or AssertJ's readable assertThat(x).isEqualTo(y).
  • @BeforeEach runs before every test; tests must not depend on each other.
  • @ParameterizedTest runs one test with many inputs.
  • @DisplayName and @Nested make reports easier to read.

Good unit tests are fast (milliseconds), isolated (no database or network) and deterministic (same result every run). Run them with ./mvnw test, and in CI on every push.

Example

Java
class PricingTest {

    private Pricing pricing;

    @BeforeEach
    void setUp() {
        pricing = new Pricing(new BigDecimal("0.18"));             // arrange
    }

    @Test
    void addsGstToThePrice() {
        BigDecimal total = pricing.withGst(new BigDecimal("1000")); // act
        assertEquals(new BigDecimal("1180.00"), total);             // assert
    }

    @Test
    void rejectsNegativePrices() {
        var ex = assertThrows(IllegalArgumentException.class,
                () -> pricing.withGst(new BigDecimal("-1")));
        assertTrue(ex.getMessage().contains("negative"));
    }

    @ParameterizedTest
    @CsvSource({"STUDENT, 500.00", "FESTIVE, 800.00", "NONE, 1000.00"})
    void appliesDiscounts(Discount discount, String expected) {
        assertEquals(new BigDecimal(expected), pricing.discounted(new BigDecimal("1000"), discount));
    }
}

Common mistake

Tests that depend on each other's order or on shared static state. Each test must set up what it needs and pass on its own.

Under the hood

Test behaviour through public methods rather than private details, so refactoring doesn't break tests. Aim for meaningful coverage of business rules and edge cases (nulls, empty lists, boundaries) rather than a coverage percentage. Inject a Clock instead of calling now() so time-based rules are testable. JUnit 6 (2025) continues the same Jupiter API with Java 17 as its minimum.

Check yourself

Which JUnit 5 annotation runs before each test?

How this connects

Part of Job-ready backend developer.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.