diff --git a/runtime/citrus-junit5/pom.xml b/runtime/citrus-junit5/pom.xml
index 565e920e34..522b5ae501 100644
--- a/runtime/citrus-junit5/pom.xml
+++ b/runtime/citrus-junit5/pom.xml
@@ -44,6 +44,10 @@
junit-jupiter-engine
compile
+
+ org.junit.platform
+ junit-platform-launcher
+
diff --git a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/JUnitJupiterEngine.java b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/JUnitJupiterEngine.java
new file mode 100644
index 0000000000..a2e290833a
--- /dev/null
+++ b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/JUnitJupiterEngine.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.junit.jupiter;
+
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import org.citrusframework.TestClass;
+import org.citrusframework.TestSource;
+import org.citrusframework.junit.jupiter.main.JUnitCitrusTest;
+import org.citrusframework.main.AbstractTestEngine;
+import org.citrusframework.main.TestRunConfiguration;
+import org.citrusframework.main.scan.ClassPathTestScanner;
+import org.citrusframework.main.scan.JarFileTestScanner;
+import org.citrusframework.util.StringUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.engine.DiscoverySelector;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.LauncherSession;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
+
+public class JUnitJupiterEngine extends AbstractTestEngine {
+
+ /** Logger */
+ private static final Logger logger = LoggerFactory.getLogger(JUnitJupiterEngine.class);
+
+ private final Set testExecutionListeners = new LinkedHashSet<>();
+
+ private boolean printSummary = true;
+
+ public JUnitJupiterEngine(TestRunConfiguration configuration) {
+ super(configuration);
+ }
+
+ @Override
+ public void run() {
+ LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
+
+ if (getConfiguration().getTestSources().isEmpty()) {
+ addTestPackages(requestBuilder, getConfiguration());
+ } else {
+ addTestClasses(requestBuilder, getConfiguration());
+ addTestSources(requestBuilder, getConfiguration());
+ }
+
+ LauncherDiscoveryRequest request = requestBuilder.build();
+
+ SummaryGeneratingListener listener = null;
+ if (printSummary) {
+ listener = new SummaryGeneratingListener();
+ }
+
+ try (LauncherSession session = LauncherFactory.openSession()) {
+ Launcher launcher = session.getLauncher();
+ launcher.registerTestExecutionListeners(testExecutionListeners.toArray(TestExecutionListener[]::new));
+
+ if (printSummary) {
+ launcher.registerTestExecutionListeners(listener);
+ }
+
+ launcher.execute(request);
+ }
+
+ if (printSummary && listener != null) {
+ TestExecutionSummary summary = listener.getSummary();
+ summary.printTo(new PrintWriter(System.out));
+ }
+ }
+
+ private void addTestSources(LauncherDiscoveryRequestBuilder requestBuilder, TestRunConfiguration configuration) {
+ List testSources = configuration.getTestSources().stream()
+ .filter(source -> !"java".equals(source.getType()))
+ .toList();
+
+ for (TestSource source : testSources) {
+ logger.info(String.format("Running test source %s", source.getName()));
+
+ JUnitCitrusTest.setSourceName(source.getName());
+ JUnitCitrusTest.setSource(Optional.ofNullable(source.getFilePath()).orElse(""));
+ requestBuilder.selectors(selectClass(JUnitCitrusTest.class));
+ }
+ }
+
+ private void addTestPackages(LauncherDiscoveryRequestBuilder requestBuilder, TestRunConfiguration configuration) {
+ List packagesToRun = configuration.getPackages();
+ if (packagesToRun == null || packagesToRun.isEmpty()) {
+ packagesToRun = Collections.singletonList("");
+ logger.info("Running all tests in project");
+ }
+
+ List selectors = new ArrayList<>();
+ for (String packageName : packagesToRun) {
+ if (StringUtils.hasText(packageName)) {
+ logger.info(String.format("Running tests in package %s", packageName));
+ }
+
+ List classesToRun;
+ if (configuration.getTestJar() != null) {
+ classesToRun = new JarFileTestScanner(configuration.getTestJar(),
+ configuration.getIncludes()).findTestsInPackage(packageName);
+ } else {
+ classesToRun = new ClassPathTestScanner(Test.class, configuration.getIncludes()).findTestsInPackage(packageName);
+ }
+
+ classesToRun.stream()
+ .peek(testClass -> logger.info(String.format("Running test %s",
+ Optional.ofNullable(testClass.getMethod()).map(method -> testClass.getName() + "#" + method)
+ .orElseGet(testClass::getName))))
+ .map(testClass -> {
+ try {
+ Class> clazz;
+ if (configuration.getTestJar() != null) {
+ clazz = Class.forName(testClass.getName(), false,
+ new URLClassLoader(new URL[]{configuration.getTestJar().toURI().toURL()}, getClass().getClassLoader()));
+ } else {
+ clazz = Class.forName(testClass.getName());
+ }
+ return clazz;
+ } catch (ClassNotFoundException | MalformedURLException e) {
+ logger.warn("Unable to read test class: " + testClass.getName());
+ return Void.class;
+ }
+ })
+ .filter(clazz -> !clazz.equals(Void.class))
+ .forEach(clazz -> selectors.add(selectClass(clazz)));
+
+ requestBuilder.selectors(selectors);
+
+ logger.info(String.format("Found %s test classes to execute", selectors.size()));
+ }
+ }
+
+ private void addTestClasses(LauncherDiscoveryRequestBuilder requestBuilder, TestRunConfiguration configuration) {
+ List testClasses = configuration.getTestSources().stream()
+ .filter(source -> "java".equals(source.getType()))
+ .map(TestSource::getName)
+ .map(TestClass::fromString)
+ .toList();
+
+ List selectors = new ArrayList<>();
+ for (TestClass testClass : testClasses) {
+ logger.info(String.format("Running test %s",
+ Optional.ofNullable(testClass.getMethod()).map(method -> testClass.getName() + "#" + method)
+ .orElseGet(testClass::getName)));
+
+ try {
+ Class> clazz;
+ if (configuration.getTestJar() != null) {
+ clazz = Class.forName(testClass.getName(), false,
+ new URLClassLoader(new URL[]{configuration.getTestJar().toURI().toURL()}, getClass().getClassLoader()));
+ } else {
+ clazz = Class.forName(testClass.getName());
+ }
+
+ if (StringUtils.hasText(testClass.getMethod())) {
+ selectors.add(selectMethod(clazz, testClass.getMethod()));
+ } else {
+ selectors.add(selectClass(clazz));
+ }
+
+ } catch (ClassNotFoundException | MalformedURLException e) {
+ logger.warn("Unable to read test class: " + testClass.getName());
+ }
+ }
+
+ requestBuilder.selectors(selectors);
+ }
+
+ public JUnitJupiterEngine addTestListener(TestExecutionListener testExecutionListener) {
+ testExecutionListeners.add(testExecutionListener);
+ return this;
+ }
+
+ public JUnitJupiterEngine withPrintSummary(boolean enabled) {
+ this.printSummary = enabled;
+ return this;
+ }
+}
diff --git a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/main/JUnitCitrusTest.java b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/main/JUnitCitrusTest.java
new file mode 100644
index 0000000000..656ec0ef4c
--- /dev/null
+++ b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/main/JUnitCitrusTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.junit.jupiter.main;
+
+import org.citrusframework.Citrus;
+import org.citrusframework.TestCaseRunner;
+import org.citrusframework.annotations.CitrusAnnotations;
+import org.citrusframework.annotations.CitrusFramework;
+import org.citrusframework.annotations.CitrusResource;
+import org.citrusframework.annotations.CitrusTest;
+import org.citrusframework.common.TestLoader;
+import org.citrusframework.common.TestSourceAware;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.exceptions.CitrusRuntimeException;
+import org.citrusframework.junit.jupiter.CitrusSupport;
+import org.citrusframework.util.FileUtils;
+import org.citrusframework.util.StringUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * JUnit test wrapper to run Citrus polyglot test definitions such as XML, YAML, Groovy.
+ * Usually this test is run with TestNG engine in a main CLI.
+ * The test is provided with test name and test source file parameters usually specified as part of the JUnit launcher configuration.
+ * The test loads the polyglot Citrus test resource via respective test loader implementation.
+ *
+ * This is not supposed to be a valid base class for arbitrary Citrus Java test cases.
+ * For such as use case scenario please refer to the Citrus JUnit extension class instead.
+ */
+@CitrusSupport
+public class JUnitCitrusTest {
+
+ private static String sourceName;
+ private static String source;
+
+ @CitrusFramework
+ Citrus citrus;
+
+ @CitrusResource
+ TestCaseRunner runner;
+
+ @CitrusResource
+ TestContext context;
+
+ @Test
+ @CitrusTest
+ public void execute() {
+ String type;
+ if (StringUtils.hasText(source)) {
+ type = FileUtils.getFileExtension(source);
+ } else {
+ type = FileUtils.getFileExtension(sourceName);
+ }
+
+ TestLoader testLoader = TestLoader.lookup(type)
+ .orElseThrow(() -> new CitrusRuntimeException(String.format("Failed to resolve test loader for type %s", type)));
+
+ testLoader.setTestClass(this.getClass());
+ testLoader.setTestName(sourceName);
+
+ if (testLoader instanceof TestSourceAware sourceAwareTestLoader) {
+ sourceAwareTestLoader.setSource(source);
+ }
+
+ CitrusAnnotations.injectAll(testLoader, citrus, context);
+ CitrusAnnotations.injectTestRunner(testLoader, runner);
+ testLoader.load();
+ }
+
+ public static void setSourceName(String name) {
+ sourceName = name;
+ }
+
+ public static void setSource(String testSource) {
+ source = testSource;
+ }
+}
diff --git a/runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit-jupiter b/runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit-jupiter
new file mode 100644
index 0000000000..739c8bcb38
--- /dev/null
+++ b/runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit-jupiter
@@ -0,0 +1 @@
+type=org.citrusframework.junit.jupiter.JUnitJupiterEngine
diff --git a/runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit5 b/runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit5
new file mode 100644
index 0000000000..739c8bcb38
--- /dev/null
+++ b/runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit5
@@ -0,0 +1 @@
+type=org.citrusframework.junit.jupiter.JUnitJupiterEngine
diff --git a/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/JUnitJupiterEngineTest.java b/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/JUnitJupiterEngineTest.java
new file mode 100644
index 0000000000..2bc9576cab
--- /dev/null
+++ b/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/JUnitJupiterEngineTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.junit.jupiter;
+
+import java.util.Collections;
+
+import org.citrusframework.TestClass;
+import org.citrusframework.TestSource;
+import org.citrusframework.junit.jupiter.scan.SampleJUnitJupiterTest;
+import org.citrusframework.main.TestEngine;
+import org.citrusframework.main.TestRunConfiguration;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * @since 2.7.4
+ */
+public class JUnitJupiterEngineTest {
+
+ @Test
+ public void testRunPackage() {
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setIncludes(new String[] { ".*Test" });
+ configuration.setPackages(Collections.singletonList(SampleJUnitJupiterTest.class.getPackage().getName()));
+
+ runTestEngine(configuration, 1L);
+ }
+
+ @Test
+ public void testRunClass() {
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setTestSources(Collections.singletonList(new TestSource(SampleJUnitJupiterTest.class)));
+
+ runTestEngine(configuration, 1L);
+ }
+
+ @Test
+ public void testRunMethod() {
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setTestSources(Collections.singletonList(TestClass.fromString(SampleJUnitJupiterTest.class.getName() + "#sampleTest()")));
+
+ runTestEngine(configuration, 1L);
+ }
+
+ @Test
+ public void testRunNoMatch() {
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setIncludes(new String[] { ".*Foo" });
+ configuration.setPackages(Collections.singletonList(SampleJUnitJupiterTest.class.getPackage().getName()));
+
+ runTestEngine(configuration, 0L);
+ }
+
+ @Test
+ public void shouldResolveJUnitEngine() {
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setEngine("junit5");
+ Assert.assertEquals(TestEngine.lookup(configuration).getClass(), JUnitJupiterEngine.class);
+ }
+
+ private void runTestEngine(TestRunConfiguration configuration, long passed) {
+ JUnitJupiterEngine engine = new JUnitJupiterEngine(configuration);
+ engine.addTestListener(new SummaryGeneratingListener() {
+ @Override
+ public void testPlanExecutionFinished(TestPlan testPlan) {
+ super.testPlanExecutionFinished(testPlan);
+
+ Assert.assertEquals(getSummary().getTestsSucceededCount(), passed);
+ Assert.assertEquals(getSummary().getTestsFoundCount(), passed);
+ }
+ });
+ engine.run();
+ }
+}
diff --git a/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/JustLooksLikeTest.java b/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/JustLooksLikeTest.java
new file mode 100644
index 0000000000..dfd12d5a05
--- /dev/null
+++ b/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/JustLooksLikeTest.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.junit.jupiter.scan;
+
+/**
+ * @since 2.7.4
+ */
+public class JustLooksLikeTest {
+
+ public JustLooksLikeTest() {
+ }
+}
diff --git a/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/SampleJUnitJupiterTest.java b/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/SampleJUnitJupiterTest.java
new file mode 100644
index 0000000000..d73efeaddb
--- /dev/null
+++ b/runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/SampleJUnitJupiterTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.junit.jupiter.scan;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class SampleJUnitJupiterTest {
+
+ @Test
+ public void sampleTest() {
+ Assertions.assertEquals(2, 1 + 1);
+ }
+}
diff --git a/runtime/citrus-main/pom.xml b/runtime/citrus-main/pom.xml
index 4749d8c8d9..60624e3655 100644
--- a/runtime/citrus-main/pom.xml
+++ b/runtime/citrus-main/pom.xml
@@ -14,6 +14,11 @@
Citrus Main CLI
+
+ org.citrusframework
+ citrus-junit5
+ ${project.version}
+
org.citrusframework
citrus-junit
diff --git a/runtime/citrus-main/src/test/java/org/citrusframework/main/TestEngineLookupTest.java b/runtime/citrus-main/src/test/java/org/citrusframework/main/TestEngineLookupTest.java
index 290720b5ee..79e6ae6351 100644
--- a/runtime/citrus-main/src/test/java/org/citrusframework/main/TestEngineLookupTest.java
+++ b/runtime/citrus-main/src/test/java/org/citrusframework/main/TestEngineLookupTest.java
@@ -19,12 +19,27 @@
import org.citrusframework.cucumber.CucumberTestEngine;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.junit.JUnit4TestEngine;
+import org.citrusframework.junit.jupiter.JUnitJupiterEngine;
import org.citrusframework.testng.TestNGEngine;
import org.testng.Assert;
import org.testng.annotations.Test;
public class TestEngineLookupTest {
+ @Test
+ public void shouldResolveJUnit5Engine() {
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setEngine("junit5");
+ Assert.assertEquals(TestEngine.lookup(configuration).getClass(), JUnitJupiterEngine.class);
+ }
+
+ @Test
+ public void shouldResolveJUnitJupiterEngine() {
+ TestRunConfiguration configuration = new TestRunConfiguration();
+ configuration.setEngine("junit-jupiter");
+ Assert.assertEquals(TestEngine.lookup(configuration).getClass(), JUnitJupiterEngine.class);
+ }
+
@Test
public void shouldResolveJUnit4Engine() {
TestRunConfiguration configuration = new TestRunConfiguration();