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());