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?
answer
- LauncherDiscoveryRequestBuilder.request().selectors(...).filters(...)
- LauncherFactory.openSession() → Launcher
- discover() = TestPlan only; execute() = run
- SummaryGeneratingListener for counts
- execute never throws on test failure
basics
~20 sUse 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 sThe 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 linesimport 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
Know that a Launcher API exists and that build tools and IDEs use it rather than magic.
Walk the four steps — request with selectors, launcher from the factory, listeners, discover vs execute — and mention SummaryGeneratingListener.
Highlight that execute never throws, that all engines participate unless filtered, and how you'd derive an exit code and handle discovery failures.
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