From d61cfed4fb2ae27b14fddad2a5c3709c5c8dccc0 Mon Sep 17 00:00:00 2001 From: Thorsten Schlathoelter Date: Thu, 12 Oct 2023 12:25:06 +0200 Subject: [PATCH] fix(#1014): Add a mechanism to provide a custom test case runner --- .../TestCaseRunnerProvider.java | 27 ++++ .../citrusframework/common/TestLoader.java | 4 +- .../spi/ResourcePathTypeResolver.java | 2 +- .../DefaultTestCaseRunner.java | 14 +++ .../TestCaseRunnerFactory.java | 86 +++++++++++++ .../common/DefaultTestLoader.java | 4 +- .../META-INF/citrus/test/runner/default | 1 + .../TestCaseRunnerFactoryTest.java | 118 ++++++++++++++++++ .../TestBehaviorExecutingEndpointAdapter.java | 5 +- .../junit/jupiter/CitrusExtensionHelper.java | 5 +- .../citrusframework/testng/TestNGHelper.java | 4 +- 11 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 core/citrus-api/src/main/java/org/citrusframework/TestCaseRunnerProvider.java create mode 100644 core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java create mode 100644 core/citrus-base/src/main/resources/META-INF/citrus/test/runner/default create mode 100644 core/citrus-base/src/test/java/org/citrusframework/TestCaseRunnerFactoryTest.java diff --git a/core/citrus-api/src/main/java/org/citrusframework/TestCaseRunnerProvider.java b/core/citrus-api/src/main/java/org/citrusframework/TestCaseRunnerProvider.java new file mode 100644 index 0000000000..fb364dbffd --- /dev/null +++ b/core/citrus-api/src/main/java/org/citrusframework/TestCaseRunnerProvider.java @@ -0,0 +1,27 @@ +package org.citrusframework; + +import org.citrusframework.context.TestContext; + +/** + * Interface for providing TestCaseRunner. + * + * @author Thorsten Schlathoelter + * @since 4.0 + */ +public interface TestCaseRunnerProvider { + /** + * Creates a TestCaseRunner which runs the given {@link TestCase} and the given {@link TestContext}. + * @param testCase + * @param context + * @return + */ + TestCaseRunner createTestCaseRunner(TestCase testCase, TestContext context); + + /** + * Creates a TestCaseRunner with the given {@link TestContext}. + * @param context + * @return + */ + TestCaseRunner createTestCaseRunner(TestContext context); + +} diff --git a/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java b/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java index 5257ebf741..87298f9956 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java +++ b/core/citrus-api/src/main/java/org/citrusframework/common/TestLoader.java @@ -48,7 +48,7 @@ public interface TestLoader { String GROOVY = "groovy"; /** - * Loads and creates new test case object.. + * Loads and creates new test case object. * @return */ void load(); @@ -72,7 +72,7 @@ public interface TestLoader { void setPackageName(String packageName); /** - * Gets the loaded test case or null if has not been loaded yet. + * Gets the loaded test case or null if it has not been loaded yet. * @return */ TestCase getTestCase(); diff --git a/core/citrus-api/src/main/java/org/citrusframework/spi/ResourcePathTypeResolver.java b/core/citrus-api/src/main/java/org/citrusframework/spi/ResourcePathTypeResolver.java index 38973269d0..1aec0ce866 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/spi/ResourcePathTypeResolver.java +++ b/core/citrus-api/src/main/java/org/citrusframework/spi/ResourcePathTypeResolver.java @@ -31,7 +31,7 @@ /** * Type resolver resolves references via resource path lookup. Provided resource paths should point to a resource in classpath * (e.g. META-INF/my/resource/path/file-name). The resolver will try to locate the resource as classpath resource and read the file as property - * file. By default the resolver reads the default type resolver property {@link TypeResolver#DEFAULT_TYPE_PROPERTY} and instantiates a new instance + * file. By default, the resolver reads the default type resolver property {@link TypeResolver#DEFAULT_TYPE_PROPERTY} and instantiates a new instance * for the given type information. * * A possible property file content that represents the resource in classpath could look like this: diff --git a/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java b/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java index 40583698c9..68ccfb2ceb 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java +++ b/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java @@ -151,4 +151,18 @@ public void setTestCase(TestCase testCase) { this.testCase = testCase; this.testCase.setIncremental(true); } + + public static class DefaultTestCaseRunnerProvider implements TestCaseRunnerProvider { + + @Override + public TestCaseRunner createTestCaseRunner(TestContext context) { + return new DefaultTestCaseRunner(context); + } + + @Override + public TestCaseRunner createTestCaseRunner(TestCase testCase, TestContext context) { + return new DefaultTestCaseRunner(testCase, context); + } + + } } diff --git a/core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java b/core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java new file mode 100644 index 0000000000..d960cc6282 --- /dev/null +++ b/core/citrus-base/src/main/java/org/citrusframework/TestCaseRunnerFactory.java @@ -0,0 +1,86 @@ +package org.citrusframework; + +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.ResourcePathTypeResolver; + +/** + * Factory for creating {@link TestCaseRunner} instances. By default, it uses + * Citrus' built-in runner, but it also offers the flexibility to replace the default runner with a + * custom implementation. To do this, it leverages the Citrus {@link ResourcePathTypeResolver} + * mechanism. + * + * To provide a custom runner, the following file needs to be added to the classpath: + *

+ * + * 'META-INF/citrus/test/runner/custom' + * + *

+ * The specified file must define the type of {@link TestCaseRunnerProvider} responsible for + * delivering the custom test case runner. + * + * @author Thorsten Schlathoelter + * @since 4.0 + * @see TestCaseRunnerProvider + */ +public class TestCaseRunnerFactory { + + + /** The key for the default Citrus test case runner provider */ + private static final String DEFAULT = "default"; + + /** The key for a custom test case runner provider */ + private static final String CUSTOM = "custom"; + + /** Test runner resource lookup path */ + private static final String RESOURCE_PATH = "META-INF/citrus/test/runner"; + + /** Default Citrus test runner from classpath resource properties. Non-final to support testing.*/ + private ResourcePathTypeResolver typeResolver = new ResourcePathTypeResolver(RESOURCE_PATH); + + private static final TestCaseRunnerFactory INSTANCE = new TestCaseRunnerFactory(); + + private TestCaseRunnerFactory() { + // Singleton + } + + /** + * @return the Citrus default test case runner. + */ + private TestCaseRunnerProvider lookupDefault() { + return typeResolver.resolve(DEFAULT); + } + + /** + * @return a custom test case runner provider or the default, if no custom runner provider exists. + */ + private TestCaseRunnerProvider lookupCustomOrDefault() { + try { + return typeResolver.resolve(CUSTOM); + } catch (CitrusRuntimeException e) { + return lookupDefault(); + } + } + + + /** + * Create a runner. + * @param context + * @return + */ + public static TestCaseRunner createRunner(TestContext context) { + TestCaseRunnerProvider testCaseRunnerProvider = INSTANCE.lookupCustomOrDefault(); + return testCaseRunnerProvider.createTestCaseRunner(context); + } + + /** + * Create a runner. + * @param testCase + * @param context + * @return + */ + public static TestCaseRunner createRunner(TestCase testCase, TestContext context) { + TestCaseRunnerProvider testCaseRunnerProvider = INSTANCE.lookupCustomOrDefault(); + return testCaseRunnerProvider.createTestCaseRunner(testCase, context); + } +} diff --git a/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java b/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java index bec90d5358..123d358b4f 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java +++ b/core/citrus-base/src/main/java/org/citrusframework/common/DefaultTestLoader.java @@ -26,9 +26,9 @@ import org.citrusframework.Citrus; import org.citrusframework.CitrusContext; import org.citrusframework.DefaultTestCase; -import org.citrusframework.DefaultTestCaseRunner; import org.citrusframework.TestCase; import org.citrusframework.TestCaseRunner; +import org.citrusframework.TestCaseRunnerFactory; import org.citrusframework.TestResult; import org.citrusframework.annotations.CitrusFramework; import org.citrusframework.annotations.CitrusResource; @@ -140,7 +140,7 @@ protected void initializeTestRunner() { testCase = new DefaultTestCase(); } - runner = new DefaultTestCaseRunner(testCase, context); + runner = TestCaseRunnerFactory.createRunner(testCase, context); } if (testClass == null) { diff --git a/core/citrus-base/src/main/resources/META-INF/citrus/test/runner/default b/core/citrus-base/src/main/resources/META-INF/citrus/test/runner/default new file mode 100644 index 0000000000..2edc98d6f3 --- /dev/null +++ b/core/citrus-base/src/main/resources/META-INF/citrus/test/runner/default @@ -0,0 +1 @@ +type=org.citrusframework.DefaultTestCaseRunner$DefaultTestCaseRunnerProvider \ No newline at end of file diff --git a/core/citrus-base/src/test/java/org/citrusframework/TestCaseRunnerFactoryTest.java b/core/citrus-base/src/test/java/org/citrusframework/TestCaseRunnerFactoryTest.java new file mode 100644 index 0000000000..a67750d84b --- /dev/null +++ b/core/citrus-base/src/test/java/org/citrusframework/TestCaseRunnerFactoryTest.java @@ -0,0 +1,118 @@ +package org.citrusframework; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import org.citrusframework.context.TestContext; +import org.citrusframework.spi.ResourcePathTypeResolver; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class TestCaseRunnerFactoryTest { + + @Test + public void testDefaultRunnerWithGivenContext() { + TestContext testContext = new TestContext(); + TestCaseRunner runner = TestCaseRunnerFactory.createRunner(testContext); + assertEquals(runner.getClass(), DefaultTestCaseRunner.class); + + DefaultTestCaseRunner defaultTestCaseRunner = (DefaultTestCaseRunner) runner; + assertEquals(defaultTestCaseRunner.getContext(), testContext); + assertTrue(defaultTestCaseRunner.getTestCase() instanceof DefaultTestCase); + } + + @Test + public void testDefaultRunnerWithGivenTestCaseAndContext() { + TestContext testContext = new TestContext(); + TestCase testCase = new DefaultTestCase(); + + TestCaseRunner runner = TestCaseRunnerFactory.createRunner(testCase, testContext); + assertEquals(runner.getClass(), DefaultTestCaseRunner.class); + + DefaultTestCaseRunner defaultTestCaseRunner = (DefaultTestCaseRunner) runner; + assertEquals(defaultTestCaseRunner.getContext(), testContext); + assertEquals(defaultTestCaseRunner.getTestCase(), testCase); + } + + @Test + public void testCustomRunnerGivenContext() { + ResourcePathTypeResolver resolverMock = Mockito.mock(ResourcePathTypeResolver.class); + + Mockito.doReturn(new CustomTestCaseRunnerProvider()).when(resolverMock).resolve("custom"); + TestCaseRunnerFactory instance = (TestCaseRunnerFactory) ReflectionTestUtils.getField( + TestCaseRunnerFactory.class,"INSTANCE"); + Assert.assertNotNull(instance); + + TestContext testContext = new TestContext(); + + Object currentResolver = ReflectionTestUtils.getField(instance, "typeResolver"); + try { + ReflectionTestUtils.setField(instance, "typeResolver", resolverMock); + TestCaseRunner runner = TestCaseRunnerFactory.createRunner(testContext); + + assertEquals(runner.getClass(), CustomTestCaseRunner.class); + + CustomTestCaseRunner defaultTestCaseRunner = (CustomTestCaseRunner) runner; + assertEquals(defaultTestCaseRunner.getContext(), testContext); + + } finally { + ReflectionTestUtils.setField(instance, "typeResolver", currentResolver); + } + + } + + @Test + public void testCustomRunnerGivenTestCaseAndContext() { + ResourcePathTypeResolver resolverMock = Mockito.mock(ResourcePathTypeResolver.class); + + Mockito.doReturn(new CustomTestCaseRunnerProvider()).when(resolverMock).resolve("custom"); + TestCaseRunnerFactory instance = (TestCaseRunnerFactory) ReflectionTestUtils.getField( + TestCaseRunnerFactory.class,"INSTANCE"); + Assert.assertNotNull(instance); + + TestContext testContext = new TestContext(); + TestCase testCase = new DefaultTestCase(); + + Object currentResolver = ReflectionTestUtils.getField(instance, "typeResolver"); + try { + ReflectionTestUtils.setField(instance, "typeResolver", resolverMock); + TestCaseRunner runner = TestCaseRunnerFactory.createRunner(testCase, testContext); + + assertEquals(runner.getClass(), CustomTestCaseRunner.class); + + CustomTestCaseRunner defaultTestCaseRunner = (CustomTestCaseRunner) runner; + assertEquals(defaultTestCaseRunner.getContext(), testContext); + assertEquals(defaultTestCaseRunner.getTestCase(), testCase); + + } finally { + ReflectionTestUtils.setField(instance, "typeResolver", currentResolver); + } + + } + + private static class CustomTestCaseRunnerProvider implements TestCaseRunnerProvider { + + @Override + public TestCaseRunner createTestCaseRunner(TestCase testCase, TestContext context) { + return new CustomTestCaseRunner(testCase, context); + } + + @Override + public TestCaseRunner createTestCaseRunner(TestContext context) { + return new CustomTestCaseRunner(context); + } + } + + private static class CustomTestCaseRunner extends DefaultTestCaseRunner { + + public CustomTestCaseRunner(TestContext context) { + super(context); + } + + public CustomTestCaseRunner(TestCase testCase, TestContext context) { + super(testCase, context); + } + } +} diff --git a/core/citrus-spring/src/main/java/org/citrusframework/endpoint/adapter/TestBehaviorExecutingEndpointAdapter.java b/core/citrus-spring/src/main/java/org/citrusframework/endpoint/adapter/TestBehaviorExecutingEndpointAdapter.java index 1e4bc53468..9a229a2f57 100755 --- a/core/citrus-spring/src/main/java/org/citrusframework/endpoint/adapter/TestBehaviorExecutingEndpointAdapter.java +++ b/core/citrus-spring/src/main/java/org/citrusframework/endpoint/adapter/TestBehaviorExecutingEndpointAdapter.java @@ -16,9 +16,10 @@ package org.citrusframework.endpoint.adapter; -import org.citrusframework.DefaultTestCaseRunner; import org.citrusframework.TestBehavior; import org.citrusframework.TestCase; +import org.citrusframework.TestCaseRunner; +import org.citrusframework.TestCaseRunnerFactory; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.message.Message; @@ -47,7 +48,7 @@ public Message dispatchMessage(final Message request, String mappingName) { getTaskExecutor().execute(() -> { prepareExecution(request, behavior); TestContext context = getTestContext(); - DefaultTestCaseRunner testCaseRunner = new DefaultTestCaseRunner(context); + TestCaseRunner testCaseRunner = TestCaseRunnerFactory.createRunner(context); behavior.apply(testCaseRunner); }); diff --git a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtensionHelper.java b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtensionHelper.java index 0f58b95de2..270f57443e 100644 --- a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtensionHelper.java +++ b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtensionHelper.java @@ -23,11 +23,11 @@ import org.citrusframework.Citrus; import org.citrusframework.DefaultTestCase; -import org.citrusframework.DefaultTestCaseRunner; import org.citrusframework.GherkinTestActionRunner; import org.citrusframework.TestActionRunner; import org.citrusframework.TestCase; import org.citrusframework.TestCaseRunner; +import org.citrusframework.TestCaseRunnerFactory; import org.citrusframework.annotations.CitrusAnnotations; import org.citrusframework.annotations.CitrusTest; import org.citrusframework.annotations.CitrusTestSource; @@ -83,7 +83,8 @@ public static boolean isTestSourceMethod(Method method) { * @return */ public static TestCaseRunner createTestRunner(String testName, ExtensionContext extensionContext) { - TestCaseRunner testCaseRunner = new DefaultTestCaseRunner(new DefaultTestCase(), getTestContext(extensionContext)); + TestCaseRunner testCaseRunner = TestCaseRunnerFactory.createRunner( + new DefaultTestCase(), getTestContext(extensionContext)); testCaseRunner.testClass(extensionContext.getRequiredTestClass()); testCaseRunner.name(testName); testCaseRunner.packageName(extensionContext.getRequiredTestClass().getPackage().getName()); diff --git a/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java b/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java index 9644938ccf..73c9744f87 100644 --- a/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java +++ b/runtime/citrus-testng/src/main/java/org/citrusframework/testng/TestNGHelper.java @@ -29,8 +29,8 @@ import org.citrusframework.CitrusSettings; import org.citrusframework.DefaultTestCase; -import org.citrusframework.DefaultTestCaseRunner; import org.citrusframework.TestCaseRunner; +import org.citrusframework.TestCaseRunnerFactory; import org.citrusframework.annotations.CitrusTest; import org.citrusframework.annotations.CitrusTestSource; import org.citrusframework.annotations.CitrusXmlTest; @@ -90,7 +90,7 @@ public static void invokeTestMethod(Object target, ITestResult testResult, Metho * @return */ public static TestCaseRunner createTestCaseRunner(Object target, Method method, TestContext context) { - TestCaseRunner testCaseRunner = new DefaultTestCaseRunner(new DefaultTestCase(), context); + TestCaseRunner testCaseRunner = TestCaseRunnerFactory.createRunner(new DefaultTestCase(), context); testCaseRunner.testClass(target.getClass()); testCaseRunner.name(target.getClass().getSimpleName()); testCaseRunner.packageName(target.getClass().getPackage().getName());