JUnit 5’s extension model replaces JUnit 4’s rules and runners. Extensions hook into the test lifecycle to add custom behavior.

Built-in Extensions

@ExtendWith — Register Extensions

  @ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserRepository repository;
    @InjectMocks UserService userService;
}
  

TempDir — Temporary Directories

  @Test
void shouldWriteFile(@TempDir Path tempDir) throws IOException {
    Path file = tempDir.resolve("output.txt");
    Files.writeString(file, "Hello");
    assertEquals("Hello", Files.readString(file));
}
// tempDir is automatically cleaned up after the test
  

RepeatedTest

  @RepeatedTest(5)
void shouldBeReliable() {
    assertTrue(service.isAvailable());
}

@RepeatedTest(value = 3, name = "{displayName} - repetition {currentRepetition}/{totalRepetitions}")
void repeatedWithName(RepetitionInfo info) {
    System.out.println("Run " + info.getCurrentRepetition());
}
  

Conditional Execution

  @Test
@EnabledOnOs(OS.LINUX)
void linuxOnlyTest() { }

@Test
@DisabledOnOs(OS.WINDOWS)
void notOnWindows() { }

@Test
@EnabledIfSystemProperty(named = "env", matches = "ci")
void ciOnlyTest() { }

@Test
@EnabledIfEnvironmentVariable(named = "RUN_INTEGRATION", matches = "true")
void integrationTest() { }
  

Custom condition:

  @Test
@EnabledIf("customCondition")
void conditionalTest() {
    boolean customCondition() {
        return Database.isAvailable();
    }
}
  

Writing Custom Extensions

  public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private long startTime;

    @Override
    public void beforeTestExecution(ExtensionContext context) {
        startTime = System.currentTimeMillis();
    }

    @Override
    public void afterTestExecution(ExtensionContext context) {
        long duration = System.currentTimeMillis() - startTime;
        System.out.println(context.getDisplayName() + " took " + duration + "ms");
    }
}
  

Usage:

  @ExtendWith(TimingExtension.class)
class TimedTests {
    @Test
    void slowTest() throws InterruptedException {
        Thread.sleep(100);
    }
}
  

Global Extension Registration

Apply to all tests via META-INF/services:

src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension:

  com.example.TimingExtension
  

Or in junit-platform.properties:

  junit.jupiter.extensions.autodetection.enabled=true
  

Callback Order

  BeforeAll → BeforeEach → BeforeTestExecution → Test → AfterTestExecution → AfterEach → AfterAll
  

Extensions can implement multiple callback interfaces in one class.

Best Practices

  • Use built-in extensions (TempDir, MockitoExtension) before writing custom ones
  • Register global extensions sparingly — prefer explicit @ExtendWith
  • Use conditional annotations to skip tests in unsupported environments
  • Custom extensions should be stateless or use ExtensionContext.Store for state
  • Prefer extensions over JUnit 4 @Rule patterns