skip to content

TestEngine & Launcher API

The SPI that lets any framework run on the Platform and the Launcher that IDEs and build tools call. Asked in tooling-heavy interviews and engine-authoring discussions.

on this pageshow

questions

5

You need to discover and run JUnit tests from your own Java code — for example inside a custom tool that picks which tests to run — rather than letting a build tool do it. Which JUnit Platform API do you use, and what are the steps?

level: middleimportance: must knowfreq 30%

answer

  1. LauncherDiscoveryRequestBuilder.request().selectors(...).filters(...)
  2. LauncherFactory.openSession() → Launcher
  3. discover() = TestPlan only; execute() = run
  4. SummaryGeneratingListener for counts
  5. execute never throws on test failure

basics

~20 s

Use the JUnit Platform Launcher API from junit-platform-launcher: build a LauncherDiscoveryRequest with selectors and filters, open a LauncherSession to get a Launcher, register a TestExecutionListener, then call discover() to inspect the TestPlan or execute() to run it.

solid answer

~40 s

The entry point is `junit-platform-launcher` — the same API IDEs and build tools use. Steps: 1. **Build a request** with `LauncherDiscoveryRequestBuilder.request()`, adding *selectors* (`selectPackage`, `selectClass`, `selectMethod`, `selectClasspathRoots`, `selectUniqueId`), optional *filters* (`includeClassNamePatterns`, `includeTags`, `includeEngines`), and configuration parameters. 2. **Get a Launcher** via `LauncherFactory.openSession()` (a `LauncherSession`, which also drives `LauncherSessionListener`s) or `LauncherFactory.create()`. 3. **Register listeners** — e.g. `SummaryGeneratingListener`, or your own `TestExecutionListener`. 4. **`launcher.discover(request)`** returns a `TestPlan` without running anything — perfect for "what would run?" tooling. 5. **`launcher.execute(request, listeners…)`** discovers *and* runs, emitting started/skipped/finished events to listeners. The Launcher is engine-agnostic: whatever engines are on the classpath (Jupiter, vintage, Cucumber) all participate, and results arrive in one merged `TestPlan`.

code

java · 30 lines
java
import org.junit.platform.launcher.*;
import org.junit.platform.launcher.core.*;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import java.io.PrintWriter;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
import static org.junit.platform.launcher.EngineFilter.includeEngines;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;

public class MiniRunner {

    public static void main(String[] args) {
        LauncherDiscoveryRequest req = request()
                .selectors(selectPackage("com.example.orders"))
                .filters(includeEngines("junit-jupiter"))
                .configurationParameter("junit.jupiter.execution.parallel.enabled", "true")
                .build();

        SummaryGeneratingListener listener = new SummaryGeneratingListener();
        try (LauncherSession session = LauncherFactory.openSession()) {
            Launcher launcher = session.getLauncher();
            launcher.registerTestExecutionListeners(listener);
            launcher.execute(req);
        }

        TestExecutionSummary summary = listener.getSummary();
        summary.printTo(new PrintWriter(System.out));
        System.exit(summary.getTotalFailureCount() == 0 ? 0 : 1);
    }
}

go deeper

for a junior

Know that a Launcher API exists and that build tools and IDEs use it rather than magic.

for a middle

Walk the four steps — request with selectors, launcher from the factory, listeners, discover vs execute — and mention SummaryGeneratingListener.

for a senior

Highlight that execute never throws, that all engines participate unless filtered, and how you'd derive an exit code and handle discovery failures.

for a principal

Position it as the integration seam for test strategy — change-based selection, quarantine reruns by unique id, org-wide reporting — and weigh building on it versus using the console launcher.

## What the Launcher API is for `junit-platform-launcher` is the **client-facing** side of the JUnit Platform. Engines implement the `TestEngine` SPI; tools consume the `Launcher`. Everything that runs JUnit tests — IDE test runners, build-tool integrations, the console launcher — goes through this API. You reach for it directly when you are writing a tool: a selective test runner driven by a change set, a flakiness harness that reruns specific ids, a report generator, or an in-process test executor inside another application. ## The four moving parts **1. `LauncherDiscoveryRequest`** — what to look for. Built with `LauncherDiscoveryRequestBuilder`: - **Selectors** (`org.junit.platform.engine.discovery.DiscoverySelectors`) say *where* to look: `selectClasspathRoots(...)`, `selectPackage("com.example")`, `selectClass(CartTest.class)`, `selectMethod(CartTest.class, "total")`, `selectUniqueId("[engine:junit-jupiter]/[class:...]/[method:...]")`, plus file/directory/module/URI variants. - **Filters** narrow the result: `includeClassNamePatterns`, `excludeClassNamePatterns`, `includePackageNames`, `includeTags`/`excludeTags`, `includeEngines`/`excludeEngines`. - **Configuration parameters** pass engine settings (`configurationParameter("junit.jupiter.execution.parallel.enabled", "true")`). **2. `Launcher`** — obtained from `LauncherFactory`. Prefer `LauncherFactory.openSession()`, which returns a `LauncherSession` in a try-with-resources block: opening and closing the session fires `LauncherSessionListener` callbacks, the hook frameworks use for once-per-session resources. `LauncherFactory.create()` gives a bare `Launcher` without session semantics. **3. `TestPlan`** — the immutable tree of `TestIdentifier`s produced by discovery. Each identifier exposes a display name, a `UniqueId`, tags, a source, and whether it is a test or a container. `discover(request)` gives you this **without executing anything**, which is exactly what "list what would run" tooling needs. **4. `TestExecutionListener`** — the event sink during execution: `testPlanExecutionStarted`, `executionStarted`, `executionSkipped`, `executionFinished(identifier, TestExecutionResult)`, `reportingEntryPublished`, `testPlanExecutionFinished`. `SummaryGeneratingListener` is a ready-made one that accumulates counts and failures. ## The canonical flow ```java LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors(selectPackage("com.example.orders")) .filters(includeClassNamePatterns(".*Test")) .build(); try (LauncherSession session = LauncherFactory.openSession()) { Launcher launcher = session.getLauncher(); SummaryGeneratingListener summary = new SummaryGeneratingListener(); launcher.registerTestExecutionListeners(summary); TestPlan plan = launcher.discover(request); // optional: inspect first launcher.execute(request); // discovers again, then runs summary.getSummary().printTo(new PrintWriter(System.out)); } ``` Note that `execute(request)` performs its own discovery; calling `discover` first is for inspection, not a prerequisite. There is also an overload that executes a previously obtained `TestPlan`. ## Things people get wrong **Expecting `execute` to throw on failure.** It does not. Test failures are reported to listeners, not raised as exceptions; the method returns normally even if everything failed. A custom runner must inspect results (e.g. `summary.getSummary().getTotalFailureCount()`) and decide its own exit code. Forgetting this produces a tool that always reports success. **Forgetting the launcher artifact.** Ordinary test code never mentions `Launcher` types, so a project that suddenly drives the Launcher itself needs `junit-platform-launcher` compile-visible, plus at least one engine at runtime. **Assuming only Jupiter runs.** The Launcher broadcasts discovery to every registered engine. If the vintage or Cucumber engine is present, their tests join the same plan — use `includeEngines("junit-jupiter")` if you mean only one. **Ignoring discovery-time failures.** If an engine fails during discovery, the plan contains a failed container rather than throwing; a robust tool checks for that instead of assuming an empty result means "no tests". ## When *not* to use it Inside an ordinary test suite, never — the build tool already owns the launcher. Reach for it when the *selection* or *reporting* logic is the product: change-based test selection, flaky-test quarantining that reruns unique ids, a custom console for a bespoke pipeline, or embedding test execution in an application. Also consider the console launcher (`junit-platform-console`) when a command line is enough; it is this same API with a CLI wrapped around it.

  • Does `launcher.execute(...)` throw when tests fail?
    No. Failures are delivered to registered `TestExecutionListener`s as `executionFinished` results with a FAILED status; `execute` returns normally. A programmatic runner must aggregate results itself — for example via `SummaryGeneratingListener` — and translate them into an exit code or exception. Assuming otherwise yields a tool that always reports success.
  • What is the difference between `launcher.discover(request)` and `launcher.execute(request)`?
    `discover` runs only the discovery phase and returns an immutable `TestPlan`, so you can list, count or filter what would run without side effects. `execute` performs discovery and then executes, streaming events to listeners. `execute` does not require a prior `discover` call — it discovers internally.
  • Why use `LauncherFactory.openSession()` instead of `LauncherFactory.create()`?
    `openSession()` returns a `LauncherSession` whose open/close fires `LauncherSessionListener` callbacks, which is the hook for once-per-session resources such as starting a shared container or a test server. With `create()` there is no session boundary, so those listeners never run.

saying these in an interview costs you the question

  • Expecting execute() to throw an exception when a test fails
  • Thinking discover() also runs the tests
  • Assuming only Jupiter tests are discovered regardless of engines present
  • Believing ordinary test classes need junit-platform-launcher on the compile path
  • Registering listeners after execute() and wondering why nothing was captured

context

open as a page

When you build a JUnit Platform discovery request you can add both selectors and filters. What is the difference between them, and at what point in a run is each one applied?

level: middleimportance: should knowfreq 22%

basics

~20 s

Selectors say where to look — a package, class, method, classpath root or unique id — and engines interpret them while discovering. Filters remove things from what was found: class-name and package filters during discovery, engine and tag filters applied by the launcher around it.

open as a page

The JUnit Platform's TestEngine interface splits work into a discovery phase and an execution phase. What does each phase produce, and why is discovery a separate step at all?

level: seniorimportance: should knowfreq 25%

basics

~20 s

discover() inspects the request and returns a tree of TestDescriptors with unique ids, running no test code. execute() then runs that tree, reporting started/skipped/finished events. Separating them lets tools list, count, filter and address tests before anything executes.

open as a page

You want a custom live console line for every test as it finishes, and separately you want one shared Docker container started before the first test in a JVM and stopped after the last one. Which JUnit Platform listener interfaces cover each need, and how are they registered?

level: seniorimportance: should knowfreq 20%

basics

~10 s

Per-test reporting uses a TestExecutionListener, which receives testPlanExecutionStarted, executionStarted/skipped/finished and testPlanExecutionFinished. One-per-JVM setup uses a LauncherSessionListener, whose launcherSessionOpened/Closed bracket the whole session. Both are registered automatically via ServiceLoader, or explicitly on the Launcher.

open as a page

Under what circumstances would you implement a custom JUnit Platform TestEngine, rather than solving the problem inside an existing testing framework's programming model?

level: principalimportance: nice to knowfreq 14%

basics

~20 s

Write an engine only when your tests are not Java methods with annotations — for example specs in files, generated cases, or a different execution model. If tests are still ordinary annotated methods, an extension or a parameterised source is the right level and far cheaper.

open as a page