From feb7f1fd3f2ee184ef5ad1a0e19a5e0d12ec681c Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Thu, 7 Nov 2024 15:06:36 +0100 Subject: [PATCH] chore: Add JUnit Jupiter test engine implementation --- runtime/citrus-junit5/pom.xml | 4 + .../junit/jupiter/JUnitJupiterEngine.java | 210 ++++++++++++++++++ .../junit/jupiter/main/JUnitCitrusTest.java | 90 ++++++++ .../META-INF/citrus/engine/junit-jupiter | 1 + .../resources/META-INF/citrus/engine/junit5 | 1 + .../junit/jupiter/JUnitJupiterEngineTest.java | 90 ++++++++ .../junit/jupiter/scan/JustLooksLikeTest.java | 26 +++ .../jupiter/scan/SampleJUnitJupiterTest.java | 28 +++ runtime/citrus-main/pom.xml | 5 + .../main/TestEngineLookupTest.java | 15 ++ 10 files changed, 470 insertions(+) create mode 100644 runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/JUnitJupiterEngine.java create mode 100644 runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/main/JUnitCitrusTest.java create mode 100644 runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit-jupiter create mode 100644 runtime/citrus-junit5/src/main/resources/META-INF/citrus/engine/junit5 create mode 100644 runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/JUnitJupiterEngineTest.java create mode 100644 runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/JustLooksLikeTest.java create mode 100644 runtime/citrus-junit5/src/test/java/org/citrusframework/junit/jupiter/scan/SampleJUnitJupiterTest.java 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();