skip to content

JUnit 5 can pick up extensions from the classpath automatically via the ServiceLoader. How is that turned on, what does an extension author have to publish, and why is it usually not the default choice?

level: seniorimportance: nice to knowfreq 16%

answer

  1. META-INF/services/org.junit.jupiter.api.extension.Extension
  2. junit.jupiter.extensions.autodetection.enabled=true
  3. junit-platform.properties / -D / discovery request
  4. Global to every test, registered before explicit ones
  5. No-arg constructor, no configuration, no per-test opt-out

basics

~20 s

Set the configuration parameter junit.jupiter.extensions.autodetection.enabled=true (in junit-platform.properties, as a system property, or in the discovery request). The extension JAR must declare its class in META-INF/services/org.junit.jupiter.api.extension.Extension. It then applies to every test, which is why it is opt-in and off by default.

solid answer

~50 s

Jupiter has a third registration path beyond `@ExtendWith` and `@RegisterExtension`: **automatic detection via the JDK `ServiceLoader`**. - The extension author ships a file `META-INF/services/org.junit.jupiter.api.extension.Extension` listing implementation class names. - The consumer opts in with the configuration parameter `junit.jupiter.extensions.autodetection.enabled=true` — typically in `src/test/resources/junit-platform.properties`, or as a `-D` system property, or set in a `LauncherDiscoveryRequest`. When enabled, every such extension on the test classpath is registered **globally, for every test in the run**, and is registered *before* declarative and programmatic extensions. It is off by default deliberately. Global, implicit behaviour changes are hard to reason about: adding a dependency can silently alter every test, the extension is still built with a no-arg constructor so it cannot be configured, and ordering relative to other extensions is not something the test author controls. It is right for cross-cutting infrastructure a whole organisation wants unconditionally — timing, tracing, a report-entry publisher — and wrong for anything a test should opt into.

code

java · 11 lines
java
// src/test/resources/junit-platform.properties
// junit.jupiter.extensions.autodetection.enabled=true

// META-INF/services/org.junit.jupiter.api.extension.Extension
// com.example.testing.TimingExtension

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    public TimingExtension() { }
    @Override public void beforeTestExecution(ExtensionContext ctx) { /* record start */ }
    @Override public void afterTestExecution(ExtensionContext ctx) { /* publish duration */ }
}

go deeper

for a junior

Knowing that a third, classpath-based registration path exists and is off by default is enough.

for a middle

Name the property, the META-INF/services file, and the fact that it applies globally with no configuration hook.

for a senior

Weigh it against meta-annotations, explain the ordering (auto-detected wrap explicit) and how to debug an extension nobody registered.

for a principal

Decide policy: which cross-cutting guards deserve to be unavoidable org-wide versus opt-in, and how that interacts with dependency hygiene in a shared test-support library.

## The three registration paths Jupiter registers extensions in three ways: **declaratively** with `@ExtendWith`, **programmatically** with `@RegisterExtension`, and **automatically** through Java's `ServiceLoader`. The first two are explicit at the test; the third is invisible from the test source, which is both its point and its danger. ## What the extension author publishes The `ServiceLoader` contract: put a plain-text file at ``` META-INF/services/org.junit.jupiter.api.extension.Extension ``` inside the JAR, containing one fully-qualified implementation class name per line. Each listed class must implement `Extension` (via one or more callback interfaces) and must have a public no-arg constructor, because `ServiceLoader` instantiates it reflectively. ## What the consumer does Auto-detection is **disabled by default**. Turn it on with the configuration parameter: ```properties # src/test/resources/junit-platform.properties junit.jupiter.extensions.autodetection.enabled=true ``` The same parameter can be supplied as a JVM system property (`-Djunit.jupiter.extensions.autodetection.enabled=true`) or programmatically in a `LauncherDiscoveryRequest`. Configuration parameters resolve from the discovery request first, then system properties, then `junit-platform.properties` on the classpath — so a build can override a checked-in default. Once enabled, **all** detected extensions apply to **every** test in the run, and they are registered ahead of declaratively and programmatically registered extensions, so their "before" callbacks run first and their "after" callbacks last. ## Why it is not the default choice 1. **Global blast radius.** There is no way to say "only these test classes". Any extension on the test classpath participates in every test, including tests written by people who have never heard of it. 2. **Implicit coupling to the dependency graph.** Adding a library — even transitively — can change test behaviour with no source change. Diagnosing "tests started failing after a dependency bump" becomes much harder. 3. **No configuration.** Like `@ExtendWith`, instances come from the no-arg constructor. Anything needing setup must read system properties or configuration parameters itself. 4. **All-or-nothing switch.** The flag enables every detected extension, not a chosen subset, so you cannot selectively adopt one service-loaded extension while ignoring another on the classpath. 5. **Order you do not control.** Auto-detected extensions always wrap the explicit ones; if you need a specific interleaving, explicit registration with `@Order` is the only reliable tool. ## When it genuinely fits - **Org-wide cross-cutting concerns**: publishing timing report entries, attaching build metadata, propagating a trace ID, enforcing a global timeout policy. - **Framework packaging**: a testing library that wants zero-configuration adoption once the consumer opts in. - **Bans and guards**: an extension that fails tests touching the real network or the real clock — something you want impossible to forget. For anything test-specific, prefer explicit registration, and prefer a project **meta-annotation** (`@IntegrationTest` carrying several `@ExtendWith`s) when you want one-line adoption without the invisibility. A meta-annotation is discoverable by grep and by IDE navigation; a service file is not. ## Debugging it If an extension you did not register is affecting tests, check three things: whether the autodetection flag is on (`junit-platform.properties`, build system properties, IDE run configuration), what `META-INF/services/org.junit.jupiter.api.extension.Extension` files exist on the test runtime classpath, and whether the effect actually comes from a meta-annotation instead. Turning the flag off is a fast way to bisect.

  • Your team wants one-line adoption of a bundle of extensions without making them global. What is the better tool?
    A composed meta-annotation: your own @IntegrationTest annotation carrying @ExtendWith({...}) plus tags and any defaults. Tests opt in explicitly with one annotation, the wiring stays greppable and navigable in the IDE, and classes that do not use it are unaffected — none of which is true of ServiceLoader auto-detection.
  • Where does JUnit look for configuration parameters like the autodetection flag, and in what precedence?
    Parameters supplied directly in the LauncherDiscoveryRequest win first, then JVM system properties, then the junit-platform.properties file on the classpath. That layering lets a repository check in a default while a specific build invocation or IDE run configuration overrides it with -D.

saying these in an interview costs you the question

  • Thinking auto-detection is on by default in JUnit 5
  • Expecting to enable only some of the detected extensions — the flag is all-or-nothing
  • Believing a service-loaded extension can be configured at registration
  • Confusing the service file name with the engine's (org.junit.platform.engine.TestEngine) service file

context