JUnitCore and Launcher API: Run JUnit Tests from Java Code

JUnitCore runs JUnit 4 tests from code. Learn its JUnit 6 replacement, the Launcher API, with selectors, tag filters, summaries, fail fast and the console.

A LauncherDiscoveryRequest with selectors and filters goes to the Launcher, which discovers tests through the Jupiter and Vintage engines, builds a TestPlan, executes it and sends events to listeners such as SummaryGeneratingListener

JUnitCore is the JUnit 4 class that runs test classes from Java code or from the command line and returns a Result with the counts and failures. In JUnit 5 and JUnit 6, the JUnit Platform Launcher API does the same job for every test engine.

We run JUnit tests programmatically when a tool decides which tests to run, for example an internal QA dashboard that starts the checkout tests against a staging server, a smoke test step inside a deployment job, or a custom runner that stops after the first failure.

The following example runs all tests of one package with the Launcher API and collects a summary.

LauncherDiscoveryRequest discoveryRequest = request()
        .selectors(selectPackage("com.howtodoinjava.junit.orders"))
        .build();

SummaryGeneratingListener listener = new SummaryGeneratingListener();
LauncherFactory.create().execute(discoveryRequest, listener);   // runs Jupiter and JUnit 4 tests
long failed = listener.getSummary().getTestsFailedCount();      // failed = 1

Notice that the request only describes which tests to run, and the listener receives the results. We start with JUnitCore for JUnit 4, move to the Launcher API with selectors, filters and summaries, stop a run after the first failure with a JUnit 6 cancellation token, and finish with the Console Launcher.

1. Running JUnit 4 Tests with JUnitCore

The JUnitCore class is a facade for running JUnit 4 and JUnit 3.8 tests. Its static method runClasses() runs the given classes and returns an org.junit.runner.Result, which holds the run count, the ignored count and a list of Failure objects.

The following example is a small online shop. OrderValidator checks the quantity against the stock and computes the shipping cost, which is free from 50 dollars. A JUnit 4 class tests the shipping rule, and a main() method runs it.

public class ShippingJUnit4Test {

    private final OrderValidator validator = new OrderValidator();

    @Test
    public void smallOrderPaysShipping() {
        assertEquals(499, validator.shippingCost(1999));
    }

    @Test
    public void largeOrderShipsFree() {
        assertEquals(0, validator.shippingCost(7500));
    }
}
Result result = JUnitCore.runClasses(ShippingJUnit4Test.class);

for (Failure failure : result.getFailures()) {
    System.out.println(failure);
}
System.out.println("Run: " + result.getRunCount()
        + ", failed: " + result.getFailureCount()
        + ", ignored: " + result.getIgnoreCount()
        + ", successful: " + result.wasSuccessful());
Run: 2, failed: 0, ignored: 0, successful: true

JUnitCore also has a main() method, so we can run classes from a terminal. It needs the compiled classes, junit-4.13.2.jar and hamcrest-core-1.3.jar on the classpath and fully qualified class names.

java -cp target/classes:target/test-classes:junit-4.13.2.jar:hamcrest-core-1.3.jar \
     org.junit.runner.JUnitCore com.howtodoinjava.junit.orders.ShippingJUnit4Test
JUnit version 4.13.2
..
Time: 0.008

OK (2 tests)

Each dot is one test. JUnitCore knows nothing about JUnit Jupiter, so it does not run @Test methods from org.junit.jupiter.api. For a codebase that mixes both, the Launcher API is the right tool, because it runs JUnit 4 classes through the Vintage engine as well.

2. The JUnit Platform Launcher API

The Launcher API is the entry point that Maven Surefire, Gradle and the IDEs use to run tests. A run has two phases. In discovery, the launcher asks every test engine on the classpath which tests match a LauncherDiscoveryRequest and builds a TestPlan. In execution, the engines run the plan and report each event to the registered TestExecutionListener objects.

A LauncherDiscoveryRequest with selectors and filters goes to the Launcher, which discovers tests through the Jupiter and Vintage engines, builds a TestPlan, executes it and sends events to listeners such as SummaryGeneratingListener
The Launcher discovers tests through every engine, then executes the plan and reports to the listeners

The API is in the junit-platform-launcher artifact, which junit-jupiter does not bring along. The junit-bom import supplies its version.

<dependency>
  <groupId>org.junit.platform</groupId>
  <artifactId>junit-platform-launcher</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.vintage</groupId>
  <artifactId>junit-vintage-engine</artifactId>
  <scope>test</scope>
</dependency>

The Vintage engine is optional and only needed for the JUnit 4 class. The runner classes of the example live in src/test/java, because they need the test classpath, and the exec-maven-plugin 3.6.4 starts them with classpathScope set to test. The examples use JUnit 6.1.3 on Java 25.

mvn -q test-compile exec:java -Dexec.mainClass=com.howtodoinjava.junit.runner.RunWithLauncher

2.1. Discovering and Executing Tests

A LauncherSession groups several discoveries and executions and releases resources when it is closed, so the recommended pattern opens it in a try-with-resources block. The following runner discovers all tests of the orders package, prints the number of tests in the plan and executes it.

LauncherDiscoveryRequest discoveryRequest = request()
        .selectors(selectPackage("com.howtodoinjava.junit.orders"))
        .build();

SummaryGeneratingListener listener = new SummaryGeneratingListener();

try (LauncherSession session = LauncherFactory.openSession()) {
    Launcher launcher = session.getLauncher();
    TestPlan testPlan = launcher.discover(discoveryRequest);
    System.out.println("Tests found: " + testPlan.countTestIdentifiers(id -> id.isTest()));
    launcher.execute(testPlan, listener);
}

TestExecutionSummary summary = listener.getSummary();
summary.printTo(new PrintWriter(System.out));
summary.printFailuresTo(new PrintWriter(System.out), 3);
Tests found: 7

Test run finished after 317 ms
[         5 containers found      ]
[         7 tests found           ]
[         0 tests skipped         ]
[         7 tests started         ]
[         6 tests successful      ]
[         1 tests failed          ]


Failures (1):
  JUnit Jupiter:RefundFailingTest:refundKeepsShippingCost()
    MethodSource [className = 'com.howtodoinjava.junit.orders.RefundFailingTest', methodName = 'refundKeepsShippingCost', methodParameterTypes = '']
    => org.opentest4j.AssertionFailedError: expected: <499> but was: <0>
       org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:569)
       com.howtodoinjava.junit.orders.RefundFailingTest.refundKeepsShippingCost(RefundFailingTest.java:12)

We can see that the plan contains seven tests from three classes, two Jupiter classes and the JUnit 4 class. RefundFailingTest fails on purpose and is excluded from mvn test in the example pom.xml. The second argument of printFailuresTo() limits the stack trace to three lines per failure.

Since JUnit 6, the Vintage engine prints an INFO message during discovery when it finds JUnit 4 classes. The example project turns it off in src/test/resources/junit-platform.properties, which the launcher reads from the classpath.

junit.vintage.discovery.issue.reporting.enabled=false

2.2. Selecting Tests with Selectors and Filters

Selectors say where to look, and filters remove tests from what the selectors found. Both come as static factory methods. The selectors are in DiscoverySelectors.

SelectorSelects
selectPackage(“com.shop”)All test classes in the package and its subpackages
selectClass(“com.shop.OrderTest”) or selectClass(OrderTest.class)One test class
selectMethod(“com.shop.OrderTest#check”)One test method
selectClasspathRoots(Set.of(path))Every test class under a classpath root, such as target/test-classes
selectUniqueId(“[engine:junit-jupiter]/…”)One node of a test plan, for example to rerun a failed test

The following runner uses a tag filter for the checkout tests and a combination of a class, a single method and a class name pattern. Tags are explained in JUnit @Tag.

LauncherDiscoveryRequest byTag = request()
        .selectors(selectPackage("com.howtodoinjava.junit.orders"))
        .filters(TagFilter.includeTags("checkout"))
        .build();

LauncherDiscoveryRequest byNames = request()
        .selectors(
                selectClass("com.howtodoinjava.junit.orders.ShippingJUnit4Test"),
                selectMethod("com.howtodoinjava.junit.orders.OrderValidatorTest#freeShippingFromFiftyDollars"))
        .filters(includeClassNamePatterns(".*Test"))
        .build();
static String run(Launcher launcher, LauncherDiscoveryRequest request) {
    SummaryGeneratingListener listener = new SummaryGeneratingListener();
    launcher.execute(request, listener);
    TestExecutionSummary summary = listener.getSummary();
    return summary.getTestsSucceededCount() + " passed, " + summary.getTestsFailedCount() + " failed";
}
checkout tag: 2 passed, 0 failed
by name:      3 passed, 0 failed

The tag filter keeps the two @Tag(“checkout”) methods and drops the failing refund test, which has no tag. A request can also carry configuration parameters through configurationParameter(key, value), for example to switch on parallel execution for one run only.

2.3. Reading the TestExecutionSummary

The TestExecutionSummary from SummaryGeneratingListener answers the common questions of a custom runner. For a richer report, or to react to each test as it finishes, we write our own TestExecutionListener, as shown in JUnit test listeners.

MethodReturns
getTestsFoundCount(), getTestsStartedCount()Tests in the plan and tests that started
getTestsSucceededCount(), getTestsFailedCount()Passed and failed tests
getTestsSkippedCount(), getTestsAbortedCount()Disabled tests and tests aborted by an assumption
getTotalFailureCount()Failed tests plus failed containers, such as a failing @BeforeAll
getFailures()A list of Failure objects with the TestIdentifier and the exception
printTo(PrintWriter), printFailuresTo(PrintWriter, int)The text report shown in the output above

3. Stopping a Run After the First Failure

A smoke test job against a staging server has no reason to continue once the login test fails, because every later test would fail for the same reason. JUnit 6 added a CancellationToken for that case. We pass it in a LauncherExecutionRequest and call cancel() from a listener, and the engines skip every test that has not started yet.

CancellationToken cancellationToken = CancellationToken.create();

TestExecutionListener cancelOnFailure = new TestExecutionListener() {
    @Override
    public void executionFinished(TestIdentifier id, TestExecutionResult result) {
        if (id.isTest() && result.getStatus() == TestExecutionResult.Status.FAILED) {
            cancellationToken.cancel();
        }
    }
};
SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
Launcher launcher = LauncherFactory.create();
launcher.execute(executionRequest(discoveryRequest)
        .listeners(cancelOnFailure, summaryListener)
        .cancellationToken(cancellationToken)
        .build());
Found 5, started 2, failed 1, skipped 3

The request selects the refund class first and the order class second. After the first failure, the launcher skips the remaining three tests and reports them as skipped. CancellationToken is an experimental API in JUnit 6.0 and later, and the Console Launcher offers the same behavior through its –fail-fast option.

4. Running Tests from the Command Line with the Console Launcher

The Console Launcher is a ready-made main() class on top of the Launcher API. The junit-platform-console-standalone jar contains the Platform, Jupiter, Vintage and Suite engines, so a build server or a Docker image can run compiled tests without Maven. Since JUnit 6, the launcher requires a subcommand such as execute or discover.

java -jar junit-platform-console-standalone-6.1.3.jar execute \
     --disable-banner --disable-ansi-colors --details-theme=ascii \
     -cp target/classes:target/test-classes \
     --select-class com.howtodoinjava.junit.orders.OrderValidatorTest
.
+-- JUnit Platform Suite [OK]
+-- JUnit Jupiter [OK]
| '-- OrderValidatorTest [OK]
|   +-- acceptsQuantityWithinStock() [OK]
|   +-- freeShippingFromFiftyDollars() [OK]
|   '-- rejectsQuantityAboveStock() [OK]
'-- JUnit Vintage [OK]

Test run finished after 111 ms
[         3 tests found           ]
[         3 tests successful      ]
[         0 tests failed          ]

The options mirror the Launcher API. For example, –select-package, –select-method and –scan-classpath are selectors, and –include-tag checkout and –include-classname are filters. The exit code is 1 when a test fails, so a shell script can stop on it.

5. JUnitCore vs Launcher API

For new code, the Launcher API replaces JUnitCore completely. JUnitCore stays useful only in a project that still runs on JUnit 4 alone.

JUnitCoreLauncher API
JUnit versionJUnit 4 (and 3.8)JUnit 5 and JUnit 6 Platform
Runs Jupiter testsNoYes
Runs JUnit 4 testsYesYes, through the Vintage engine
Select testsClasses or a RequestPackages, classes, methods, classpath roots, unique IDs
Filter by tag or nameCategories runner onlyTagFilter, ClassNameFilter, engine filters
ResultResultTestExecutionSummary or any listener
Stop on first failureNoCancellationToken (JUnit 6)
Command lineorg.junit.runner.JUnitCoreConsole Launcher

For a fixed group of classes that always runs together, a declarative @Suite class is simpler than a launcher program. The Launcher API is the better fit when the selection is computed at runtime.

6. Running Tests Programmatically FAQs

A JUnitCore runner that moves to JUnit 5 or 6 tends to raise questions about the replacement class and about a test plan that stays empty.

6.1. What Replaces JUnitCore in JUnit 5 and JUnit 6?

The Launcher API replaces it. LauncherFactory.create() or LauncherFactory.openSession() returns a Launcher, and a LauncherDiscoveryRequest plus a SummaryGeneratingListener give the same information as JUnitCore.runClasses() and Result.

6.2. Can JUnitCore Run JUnit 5 Tests?

No. JUnitCore runs only JUnit 4 and JUnit 3.8 tests. The old JUnitPlatform runner, which let JUnit 4 tools run Platform tests, came from the junit-platform-runner module, and JUnit 6 removed that module.

6.3. Why Does My Launcher Program Find Zero Tests?

In most cases a test engine is missing from the classpath or the selector points to the wrong place. The artifact junit-jupiter-engine must be on the runtime classpath, and the runner must see the compiled test classes, which is why the example runs it with the test classpath of Maven.

6.4. Do We Need junit-platform-launcher in a Normal Maven Build?

No. Surefire brings its own launcher, so a project that only runs mvn test does not declare it. We add junit-platform-launcher when our own code uses the Launcher API, and Gradle builds declare it as testRuntimeOnly.

7. Conclusion

JUnitCore.runClasses() runs JUnit 4 classes from code and returns a Result, and java org.junit.runner.JUnitCore does the same from a terminal. For JUnit 5 and JUnit 6, the Launcher API takes over. A LauncherDiscoveryRequest with selectors and filters picks the tests, a LauncherSession runs them, and a SummaryGeneratingListener collects the counts and failures.

JUnit 6 adds a CancellationToken to stop a run early, and the Console Launcher wraps the whole API in one jar for scripts. Teams that still have JUnit 4 classes can run them through the Vintage engine and plan the move with JUnit 5 vs JUnit 4. More topics are listed in the JUnit tutorial.

8. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I got an error here (test Cases) and Compiler in Eclipse doesn’t suggest me anything

    for (Class testCase : testCases)
    {
    runTestCase(testCase);
    }

    }

  2. I Used Junit Core to run specific tests now i need to get a junit report for the same, how can this be achieved? WIll be greats if u can help me

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.