Close

JUnit 5 - Custom TestEngine and Engine Filtering

[Last Updated: Dec 21, 2025]

The JUnit Platform is designed to support multiple test engines simultaneously. While most projects rely on the default Jupiter engine, custom engines can be integrated and selectively enabled or disabled using suite-level engine filters. This tutorial demonstrates a minimal custom TestEngine and shows how engine inclusion is resolved before test discovery begins.

@IncludeEngines and @ExcludeEngines

Definition of IncludeEngines

Version: 6.0.0
 package org.junit.platform.suite.api;
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 @Inherited
 @Documented
 @API(status = MAINTAINED, since = "1.0")
 public @interface IncludeEngines {
     String[] value(); 1
 }
1One or more TestEngine IDs to be included in the test plan.

Definition of ExcludeEngines

Version: 6.0.0
 package org.junit.platform.suite.api;
 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 @Inherited
 @Documented
 @API(status = MAINTAINED, since = "1.0")
 public @interface ExcludeEngines {
     String[] value(); 1
 }
1One or more TestEngine IDs to be excluded from the test plan.

What This Example Demonstrates

  • A minimal custom MyCustomTestEngine
  • The custom TestEngine will scan for the classes implementing interface CustomTest.
  • The engine will execute the method CustomTest#performTest which contains the client side test code.
  • Engine discovery will be done via the ServiceLoader mechanism
  • Selective execution of engines will be done by using @IncludeEngines and @ExcludeEngines

Example

Our custom interface for performing tests

Instead of using default @Test annotation, client side will define the test via implementing following interface.

package com.logicbig.example.engine;

public interface CustomTest {
    boolean performTest();
}

Creating our custom TestEngine

package com.logicbig.example.engine;

import org.junit.platform.engine.*;
import org.junit.platform.engine.discovery.ClassSelector;
import org.junit.platform.engine.discovery.PackageSelector;
import org.junit.platform.engine.support.descriptor.EngineDescriptor;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;

import java.lang.reflect.Modifier;
import java.util.Set;

public class MyCustomTestEngine implements TestEngine {

    public static final String ENGINE_ID = "my-simple-test-engine";

    @Override
    public String getId() {
        return ENGINE_ID;
    }

    @Override
    public TestDescriptor discover(
            EngineDiscoveryRequest discoveryRequest,
            UniqueId uniqueId
    ) {

        EngineDescriptor engineDescriptor =
                new EngineDescriptor(uniqueId, "My Custom Test Engine");

        discoveryRequest
                .getSelectorsByType(ClassSelector.class)
                .forEach(selector -> handleSuiteClass(selector, engineDescriptor));

        return engineDescriptor;
    }

    private void handleSuiteClass(
            ClassSelector selector,
            EngineDescriptor engineDescriptor
    ) {

        Class<?> suiteClass = selector.getJavaClass();

        SelectPackages selectPackages =
                suiteClass.getAnnotation(SelectPackages.class);

        if (selectPackages == null) {
            return;
        }

        for (String pkg : selectPackages.value()) {
            discoverPackage(pkg, engineDescriptor);
        }
    }

    private void discoverPackage(
            String packageName,
            EngineDescriptor engineDescriptor
    ) {

        if (!packageName.startsWith("com.logicbig.example.tests")) {
            return;
        }

        Set<Class<?>> candidates =
                ClasspathScanner.findImplementations(
                        packageName,
                        CustomTest.class
                );

        for (Class<?> testClass : candidates) {
            try {
                Object instance =
                        testClass.getDeclaredConstructor().newInstance();

                UniqueId id = engineDescriptor.getUniqueId()
                                              .append("custom-test", testClass.getName());

                engineDescriptor.addChild(
                        new CustomTestDescriptor(
                                id,
                                testClass.getSimpleName(),
                                instance
                        )
                );

            } catch (Exception ignored) {
            }
        }
    }

    @Override
    public void execute(ExecutionRequest request) {

        TestDescriptor root = request.getRootTestDescriptor();
        EngineExecutionListener listener =
                request.getEngineExecutionListener();

        listener.executionStarted(root);

        for (TestDescriptor descriptor : root.getChildren()) {

            CustomTestDescriptor testDescriptor =
                    (CustomTestDescriptor) descriptor;

            listener.executionStarted(testDescriptor);

            try {
                CustomTest test =
                        (CustomTest) testDescriptor.getTestInstance();

                boolean passed = test.performTest();

                if (!passed) {
                    throw new AssertionError(
                            "CustomTest.performTest() returned false"
                    );
                }

                listener.executionFinished(
                        testDescriptor,
                        TestExecutionResult.successful()
                );

            } catch (Throwable ex) {
                listener.executionFinished(
                        testDescriptor,
                        TestExecutionResult.failed(ex)
                );
            }
        }

        listener.executionFinished(
                root,
                TestExecutionResult.successful()
        );
    }
}

We are skipping some helper classes here, please see the project browser below for complete code.

Implementing Custom TestDescriptor

TestDescriptor describes a test. It represents a test that has been discovered by a TestEngine.

package com.logicbig.example.engine;

import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;

public class CustomTestDescriptor extends AbstractTestDescriptor {

    private final Object testInstance;

    public CustomTestDescriptor(
            UniqueId uniqueId,
            String displayName,
            Object testInstance
    ) {
        super(uniqueId, displayName);
        this.testInstance = testInstance;
    }

    public Object getTestInstance() {
        return testInstance;
    }

    @Override
    public Type getType() {
        return Type.TEST;
    }
}

Registering our custom engine

src/test/resources/META-INF/services/org.junit.platform.engine.TestEngine

com.logicbig.example.engine.MyCustomTestEngine

Our custom test class

package com.logicbig.example.tests;

import com.logicbig.example.engine.CustomTest;

public class AdditionTest implements CustomTest {
    @Override
    public boolean performTest() {
        return 2 + 2 == 4;
    }
}

Jupiter test class

package com.logicbig.example.tests;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

class JupiterTest {

    @Test
    void jupiterTestRuns() {
        assertTrue(true);
    }
}

Suite class selecting our custom engine

package com.logicbig.example.suites;

import com.logicbig.example.engine.MyCustomTestEngine;
import org.junit.platform.suite.api.*;

@Suite(failIfNoTests = false)
@SelectPackages("com.logicbig.example.tests")
@IncludeEngines(MyCustomTestEngine.ENGINE_ID)
public class CustomEngineOnlySuite {
}

Output

$ mvn test -Dtest=CustomEngineOnlySuite
[INFO] Scanning for projects...
[INFO]
[INFO] ----------< com.logicbig.example:junit-5-custom-test-engine >-----------
[INFO] Building junit-5-custom-test-engine 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-custom-test-engine ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-suite-engine\junit-5-custom-test-engine\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-custom-test-engine ---
[INFO] No sources to compile
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-custom-test-engine ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource from src\test\resources to target\test-classes
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-custom-test-engine ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- surefire:3.5.0:test (default-test) @ junit-5-custom-test-engine ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] +--My Custom Test Engine - 0.011 ss
[INFO] | '-- [OK] AdditionTest - 0.007 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.200 s
[INFO] Finished at: 2025-12-21T10:37:16+08:00
[INFO] ------------------------------------------------------------------------

The output confirms that only the custom engine participated in execution. Although the Jupiter engine is present on the classpath, the suite explicitly includes the custom engine by its identifier. As a result, no Jupiter-based tests are discovered or executed.

Selecting Jupiter and custom Engines

package com.logicbig.example.suites;

import com.logicbig.example.engine.MyCustomTestEngine;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;

@Suite(failIfNoTests = false)
@SelectPackages("com.logicbig.example.tests")
@IncludeEngines({MyCustomTestEngine.ENGINE_ID, "junit-jupiter"})
public class BothEngineSuite {
}

Output

$ mvn test -Dtest=BothEngineSuite
[INFO] Scanning for projects...
[INFO]
[INFO] ----------< com.logicbig.example:junit-5-custom-test-engine >-----------
[INFO] Building junit-5-custom-test-engine 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-custom-test-engine ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-suite-engine\junit-5-custom-test-engine\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-custom-test-engine ---
[INFO] No sources to compile
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-custom-test-engine ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource from src\test\resources to target\test-classes
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-custom-test-engine ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- surefire:3.5.0:test (default-test) @ junit-5-custom-test-engine ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] +--My Custom Test Engine - 0.085 ss
[INFO] | '-- [OK] AdditionTest - 0.011 ss
[INFO] +--BothEngineSuite JupiterTest - 0.085 ss
[INFO] | '-- [OK] jupiterTestRuns - 0.052 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.434 s
[INFO] Finished at: 2025-12-21T10:37:06+08:00
[INFO] ------------------------------------------------------------------------

As seen above, both engines were invoked and they executed their tests.

@ExcludeEngine example

In this example, we are going to extend our BothEngineSuite (which selects both engines) and exclude Jupiter engine.

package com.logicbig.example.suites;

import org.junit.platform.suite.api.ExcludeEngines;

@ExcludeEngines("junit-jupiter")
public class ExcludeEngineSuite extends BothEngineSuite {
}

Output

$ mvn test -Dtest=ExcludeEngineSuite
[INFO] Scanning for projects...
[INFO]
[INFO] ----------< com.logicbig.example:junit-5-custom-test-engine >-----------
[INFO] Building junit-5-custom-test-engine 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ junit-5-custom-test-engine ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory D:\example-projects\junit-5\junit-5-suite-engine\junit-5-custom-test-engine\src\main\resources
[INFO]
[INFO] --- compiler:3.11.0:compile (default-compile) @ junit-5-custom-test-engine ---
[INFO] No sources to compile
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ junit-5-custom-test-engine ---
[WARNING] Using platform encoding (Cp1252 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 1 resource from src\test\resources to target\test-classes
[INFO]
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ junit-5-custom-test-engine ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- surefire:3.5.0:test (default-test) @ junit-5-custom-test-engine ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] +--My Custom Test Engine - 0.012 ss
[INFO] | '-- [OK] AdditionTest - 0.008 ss
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.096 s
[INFO] Finished at: 2025-12-21T10:36:52+08:00
[INFO] ------------------------------------------------------------------------

This demonstrates that @IncludeEngines and @ExcludeEngines operate at the engine resolution phase of the JUnit Platform lifecycle. Engines that are excluded are never invoked for discovery, ensuring strict control over which testing technologies participate in a given suite execution.

Example Project

Dependencies and Technologies Used:

  • junit-platform-suite-engine 6.0.1 (Module "junit-platform-suite-engine" of JUnit)
  • junit-jupiter-engine 6.0.1 (Module "junit-jupiter-engine" of JUnit)
     Version Compatibility: 5.9.0 - 6.0.1Version List
    ×

    Version compatibilities of junit-jupiter-engine with this example:

    • 5.9.0
    • 5.9.1
    • 5.9.2
    • 5.9.3
    • 5.10.0
    • 5.10.1
    • 5.10.2
    • 5.10.3
    • 5.10.4
    • 5.10.5
    • 5.11.0
    • 5.11.1
    • 5.11.2
    • 5.11.3
    • 5.11.4
    • 5.12.0
    • 5.12.1
    • 5.12.2
    • 5.13.0
    • 5.13.1
    • 5.13.2
    • 5.13.3
    • 5.13.4
    • 5.14.0
    • 5.14.1
    • 6.0.0
    • 6.0.1

    Versions in green have been tested.

  • JDK 25
  • Maven 3.9.11

JUnit 5 - Custom TestEngine Filtering Select All Download
  • junit-5-custom-test-engine
    • src
      • test
        • java
          • com
            • logicbig
              • example
                • engine
                  • MyCustomTestEngine.java
                  • suites
                  • tests
          • resources
            • META-INF
              • services

    See Also