GH-216 - Forward an ApplicationContext's ExecutorService to Scenario instances by default.

We now register a default customizer on the Scenario instances created to pick up an ExecutorService defined in the ApplicationContext so that customizations made to that are considered in the test execution.
This commit is contained in:
Oliver Drotbohm
2023-04-20 16:31:28 +02:00
parent fb8f12fd98
commit 5dfffa2244
3 changed files with 61 additions and 2 deletions

View File

@@ -16,13 +16,17 @@
package org.springframework.modulith.test;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.function.Supplier;
import org.awaitility.core.ConditionFactory;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.util.Assert;
@@ -43,6 +47,30 @@ public interface ScenarioCustomizer extends InvocationInterceptor {
*/
Function<ConditionFactory, ConditionFactory> getDefaultCustomizer(Method method, ApplicationContext context);
/**
* Creates a default scenario customizer that will try to find an {@link ExecutorService} in the given
* {@link ApplicationContext} in the following order:
* <ol>
* <li>A unique {@link ExecutorService} bean defined</li>
* <li>A {@link ThreadPoolTaskExecutor} bean defined (the default Spring Boot creates in case no {@link Executor} is
* explicitly defined in the {@link ApplicationContext}</li>
* </ol>
*
* @param context must not be {@literal null}.
* @return will never be {@literal null}.
*/
public static Function<ConditionFactory, ConditionFactory> forwardExecutorService(ApplicationContext context) {
Supplier<ExecutorService> fallback = () -> {
var executor = context.getBeanProvider(ThreadPoolTaskExecutor.class).getIfUnique();
return executor == null ? null : executor.getThreadPoolExecutor();
};
var executorService = context.getBeanProvider(ExecutorService.class).getIfUnique(fallback);
return executorService != null ? it -> it.pollExecutorService(executorService) : Function.identity();
}
/*
* (non-Javadoc)
* @see org.junit.jupiter.api.extension.InvocationInterceptor#interceptTestTemplateMethod(org.junit.jupiter.api.extension.InvocationInterceptor.Invocation, org.junit.jupiter.api.extension.ReflectiveInvocationContext, org.junit.jupiter.api.extension.ExtensionContext)

View File

@@ -77,7 +77,8 @@ class ScenarioParameterResolver implements ParameterResolver, AfterEachCallback
var operations = resolveTransactionTemplate(context);
var events = (AssertablePublishedEvents) delegate.resolveParameter(parameterContext, extensionContext);
return new Scenario(operations, context, events);
return new Scenario(operations, context, events)
.setDefaultCustomizer(ScenarioCustomizer.forwardExecutorService(context));
}
private TransactionTemplate resolveTransactionTemplate(ApplicationContext context) {

View File

@@ -19,12 +19,17 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionFactory;
import org.awaitility.core.ExecutorLifecycle;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -50,8 +55,15 @@ class ScenarioCustomizerIntegrationTests {
TransactionTemplate transactionTemplate() {
return mock(TransactionTemplate.class);
}
@Bean
ExecutorService executorService() {
return Executors.newSingleThreadExecutor();
}
}
@Autowired ExecutorService executorService;
@BeforeEach
void setUp() {
TestScenarioCustomizer.invoked = false;
@@ -61,6 +73,8 @@ class ScenarioCustomizerIntegrationTests {
void customizerGetsAppliedForScenarioParameter(Scenario scenario) {
assertThat(TestScenarioCustomizer.invoked).isTrue();
assertThat(TestScenarioCustomizer.SAMPLE).isNotNull();
assertThat(ReflectionTestUtils.getField(scenario, "defaultCustomizer"))
.isSameAs(TestScenarioCustomizer.SAMPLE);
}
@@ -70,9 +84,23 @@ class ScenarioCustomizerIntegrationTests {
assertThat(TestScenarioCustomizer.invoked).isFalse();
}
@Test // GH-165
@SuppressWarnings("unchecked")
void forwardsExecutorServiceFromApplicationContext(Scenario scenario) {
var customizer = (Function<ConditionFactory, ConditionFactory>) ReflectionTestUtils.getField(scenario,
"defaultCustomizer");
var factory = customizer.apply(Awaitility.await());
var lifecycle = (ExecutorLifecycle) ReflectionTestUtils.getField(factory, "executorLifecycle");
assertThat(lifecycle).isNotNull();
assertThat(lifecycle.supplyExecutorService()).isEqualTo(executorService);
}
static class TestScenarioCustomizer implements ScenarioCustomizer {
static Function<ConditionFactory, ConditionFactory> SAMPLE = it -> it;
static Function<ConditionFactory, ConditionFactory> SAMPLE;
static boolean invoked = false;
@Override
@@ -81,6 +109,8 @@ class ScenarioCustomizerIntegrationTests {
invoked = true;
SAMPLE = ScenarioCustomizer.forwardExecutorService(context);
return SAMPLE;
}
}