JUnit 5 (Jupiter) is the modern testing framework for Java. It replaces JUnit 4 with a modular architecture, richer annotations, and improved assertions.

Dependencies

Maven:

  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
  

Gradle:

  testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
tasks.test { useJUnitPlatform() }
  

Basic Test

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

class CalculatorTest {

    @Test
    void shouldAddTwoNumbers() {
        Calculator calc = new Calculator();
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void shouldThrowOnDivisionByZero() {
        Calculator calc = new Calculator();
        assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
    }
}
  

Common Assertions

  assertEquals(expected, actual);
assertNotEquals(unexpected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(value);
assertNotNull(value);
assertSame(expected, actual);       // same reference
assertNotSame(unexpected, actual);
assertThrows(Exception.class, () -> { /* code */ });
assertTimeout(Duration.ofSeconds(1), () -> { /* code */ });
assertAll("person",
    () -> assertEquals("Alice", person.getName()),
    () -> assertEquals(30, person.getAge())
);
  

Test Lifecycle Annotations

  class UserServiceTest {

    @BeforeAll
    static void setupClass() {
        // runs once before all tests — must be static
    }

    @BeforeEach
    void setup() {
        // runs before each test
    }

    @AfterEach
    void teardown() {
        // runs after each test
    }

    @AfterAll
    static void teardownClass() {
        // runs once after all tests
    }

    @Test
    void shouldCreateUser() { }
}
  

Display Names and Tags

  @DisplayName("User Service Tests")
class UserServiceTest {

    @Test
    @DisplayName("Should create user with valid email")
    void createUser() { }

    @Test
    @Tag("integration")
    void integrationTest() { }

    @Test
    @Disabled("Not implemented yet")
    void futureFeature() { }
}
  

Run by tag:

  mvn test -Dgroups=integration
./gradlew test --tests "*" -Djunit.jupiter.tags=integration
  

Assumptions

Skip tests when preconditions are not met:

  @Test
void runOnlyOnLinux() {
    assumeTrue(System.getProperty("os.name").contains("Linux"));
    // test logic runs only if assumption passes
}
  

Nested Tests

  @DisplayName("Shopping Cart")
class ShoppingCartTest {

    @Nested
    @DisplayName("When empty")
    class WhenEmpty {
        @Test
        void shouldHaveZeroItems() { }
    }

    @Nested
    @DisplayName("When has items")
    class WhenHasItems {
        @BeforeEach
        void addItem() { /* setup */ }

        @Test
        void shouldCalculateTotal() { }
    }
}
  

Best Practices

  • One logical assertion per test (or use assertAll)
  • Follow naming: shouldDoSomethingWhenCondition or methodName_scenario_expectedResult
  • Use @BeforeEach for per-test setup, not shared mutable state between tests
  • Keep tests fast, independent, and repeatable
  • Use @DisplayName for readable test reports