Skip to content

Commit

Permalink
fix(#1014): Add a mechanism to provide a custom test case runner
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorsten Schlathoelter committed Oct 12, 2023
1 parent 12aa556 commit d61cfed
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
}
Original file line number Diff line number Diff line change
@@ -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:
* <p>
* <code>
* 'META-INF/citrus/test/runner/custom'
* </code>
* </p>
* 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -140,7 +140,7 @@ protected void initializeTestRunner() {
testCase = new DefaultTestCase();
}

runner = new DefaultTestCaseRunner(testCase, context);
runner = TestCaseRunnerFactory.createRunner(testCase, context);
}

if (testClass == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type=org.citrusframework.DefaultTestCaseRunner$DefaultTestCaseRunnerProvider
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit d61cfed

Please sign in to comment.