Skip to content

Commit f3f3dc6

Browse files
committed
Fix logic for validating @RecordApplicationEvents config
Prior to this commit, the SpringExtension looked up the TestInstance.Lifecycle and ExecutionMode using TestContextAnnotationUtils; however, using TestContextAnnotationUtils is problematic since the TestInstance.Lifecycle and ExecutionMode can be configured globally via configuration parameters instead of locally via the @testinstance and @execution annotations. This commit addresses these issues by looking up the TestInstance.Lifecycle and ExecutionMode via JUnit Jupiter's ExtensionContext which takes into account both global and local configuration. See gh-30020
1 parent 5e1a474 commit f3f3dc6

File tree

2 files changed

+33
-40
lines changed

2 files changed

+33
-40
lines changed

spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java

+20-23
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.junit.jupiter.api.AfterEach;
3030
import org.junit.jupiter.api.BeforeAll;
3131
import org.junit.jupiter.api.BeforeEach;
32-
import org.junit.jupiter.api.TestInstance;
32+
import org.junit.jupiter.api.TestInstance.Lifecycle;
3333
import org.junit.jupiter.api.extension.AfterAllCallback;
3434
import org.junit.jupiter.api.extension.AfterEachCallback;
3535
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
@@ -42,7 +42,6 @@
4242
import org.junit.jupiter.api.extension.ParameterContext;
4343
import org.junit.jupiter.api.extension.ParameterResolver;
4444
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
45-
import org.junit.jupiter.api.parallel.Execution;
4645
import org.junit.jupiter.api.parallel.ExecutionMode;
4746
import org.junit.platform.commons.annotation.Testable;
4847

@@ -98,7 +97,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
9897
private static final Namespace AUTOWIRED_VALIDATION_NAMESPACE =
9998
Namespace.create(SpringExtension.class.getName() + "#autowired.validation");
10099

101-
private static final String NO_AUTOWIRED_VIOLATIONS_DETECTED = "NO AUTOWIRED VIOLATIONS DETECTED";
100+
private static final String NO_VIOLATIONS_DETECTED = "NO VIOLATIONS DETECTED";
102101

103102
/**
104103
* {@link Namespace} in which {@code @RecordApplicationEvents} validation error messages
@@ -153,8 +152,8 @@ public void postProcessTestInstance(Object testInstance, ExtensionContext contex
153152
}
154153

155154
/**
156-
* Validate that test class or its enclosing class doesn't attempt to record
157-
* application events in a parallel mode that makes it un-deterministic
155+
* Validate that the test class or its enclosing class doesn't attempt to record
156+
* application events in a parallel mode that makes it non-deterministic
158157
* ({@code @TestInstance(PER_CLASS)} and {@code @Execution(CONCURRENT)}
159158
* combination).
160159
* @since 6.1.0
@@ -165,30 +164,28 @@ private void validateRecordApplicationEventsConfig(ExtensionContext context) {
165164
Store store = context.getStore(RECORD_APPLICATION_EVENTS_VALIDATION_NAMESPACE);
166165

167166
String errorMessage = store.getOrComputeIfAbsent(context.getRequiredTestClass(), testClass -> {
168-
boolean record = TestContextAnnotationUtils.hasAnnotation(testClass, RecordApplicationEvents.class);
169-
if (!record) {
170-
return NO_AUTOWIRED_VIOLATIONS_DETECTED;
167+
boolean recording = TestContextAnnotationUtils.hasAnnotation(testClass, RecordApplicationEvents.class);
168+
if (!recording) {
169+
return NO_VIOLATIONS_DETECTED;
171170
}
172-
final TestInstance testInstance = TestContextAnnotationUtils.findMergedAnnotation(testClass, TestInstance.class);
173171

174-
if (testInstance == null || testInstance.value() != TestInstance.Lifecycle.PER_CLASS) {
175-
return NO_AUTOWIRED_VIOLATIONS_DETECTED;
172+
if (context.getTestInstanceLifecycle().orElse(Lifecycle.PER_METHOD) == Lifecycle.PER_METHOD) {
173+
return NO_VIOLATIONS_DETECTED;
176174
}
177175

178-
final Execution execution = TestContextAnnotationUtils.findMergedAnnotation(testClass, Execution.class);
179-
180-
if (execution == null || execution.value() != ExecutionMode.CONCURRENT) {
181-
return NO_AUTOWIRED_VIOLATIONS_DETECTED;
176+
if (context.getExecutionMode() == ExecutionMode.SAME_THREAD) {
177+
return NO_VIOLATIONS_DETECTED;
182178
}
183179

184-
return "Test classes or inner classes that @RecordApplicationEvents must not be run in parallel "
185-
+ "with the @TestInstance(Lifecycle.PER_CLASS) configuration. Use either @Execution(SAME_THREAD), "
186-
+ "@TestInstance(PER_METHOD) or disable parallel execution altogether. Note that when recording "
187-
+ "events in parallel, one might see events published by other tests as the application context "
188-
+ "can be common.";
180+
return """
181+
Test classes or @Nested test classes that @RecordApplicationEvents must not be run \
182+
in parallel with the @TestInstance(PER_CLASS) lifecycle mode. Configure either \
183+
@Execution(SAME_THREAD) or @TestInstance(PER_METHOD) semantics, or disable parallel \
184+
execution altogether. Note that when recording events in parallel, one might see events \
185+
published by other tests since the application context may be shared.""";
189186
}, String.class);
190187

191-
if (errorMessage != NO_AUTOWIRED_VIOLATIONS_DETECTED) {
188+
if (errorMessage != NO_VIOLATIONS_DETECTED) {
192189
throw new IllegalStateException(errorMessage);
193190
}
194191
}
@@ -206,15 +203,15 @@ private void validateAutowiredConfig(ExtensionContext context) {
206203
String errorMessage = store.getOrComputeIfAbsent(context.getRequiredTestClass(), testClass -> {
207204
Method[] methodsWithErrors =
208205
ReflectionUtils.getUniqueDeclaredMethods(testClass, autowiredTestOrLifecycleMethodFilter);
209-
return (methodsWithErrors.length == 0 ? NO_AUTOWIRED_VIOLATIONS_DETECTED :
206+
return (methodsWithErrors.length == 0 ? NO_VIOLATIONS_DETECTED :
210207
String.format(
211208
"Test methods and test lifecycle methods must not be annotated with @Autowired. " +
212209
"You should instead annotate individual method parameters with @Autowired, " +
213210
"@Qualifier, or @Value. Offending methods in test class %s: %s",
214211
testClass.getName(), Arrays.toString(methodsWithErrors)));
215212
}, String.class);
216213

217-
if (errorMessage != NO_AUTOWIRED_VIOLATIONS_DETECTED) {
214+
if (errorMessage != NO_VIOLATIONS_DETECTED) {
218215
throw new IllegalStateException(errorMessage);
219216
}
220217
}

spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/ParallelApplicationEventsIntegrationTests.java

+13-17
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
import org.junit.jupiter.api.TestInfo;
3333
import org.junit.jupiter.api.TestInstance;
3434
import org.junit.jupiter.api.TestInstance.Lifecycle;
35-
import org.junit.jupiter.api.parallel.Execution;
36-
import org.junit.jupiter.api.parallel.ExecutionMode;
3735
import org.junit.platform.engine.TestExecutionResult;
3836
import org.junit.platform.testkit.engine.EngineExecutionResults;
3937
import org.junit.platform.testkit.engine.EngineTestKit;
@@ -68,36 +66,35 @@ class ParallelApplicationEventsIntegrationTests {
6866
void rejectTestsInParallelWithInstancePerClassAndRecordApplicationEvents() {
6967
Class<?> testClass = TestInstancePerClassTestCase.class;
7068

71-
final EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
69+
EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")//
7270
.selectors(selectClass(testClass))//
7371
.configurationParameter("junit.jupiter.execution.parallel.enabled", "true")//
72+
.configurationParameter("junit.jupiter.execution.parallel.mode.default", "concurrent")//
7473
.configurationParameter("junit.jupiter.execution.parallel.config.dynamic.factor", "10")//
7574
.execute();
7675

77-
//extract the messages from failed TextExecutionResults
76+
// extract the messages from failed TextExecutionResults
7877
assertThat(results.containerEvents().failed()//
79-
.stream().map(e -> e.getRequiredPayload(TestExecutionResult.class)//
80-
.getThrowable().get().getMessage()))//
78+
.map(e -> e.getRequiredPayload(TestExecutionResult.class).getThrowable().get().getMessage()))//
8179
.singleElement(InstanceOfAssertFactories.STRING)
82-
.isEqualToIgnoringNewLines("""
83-
Test classes or inner classes that @RecordApplicationEvents\s
84-
must not be run in parallel with the @TestInstance(Lifecycle.PER_CLASS) configuration.\s
85-
Use either @Execution(SAME_THREAD), @TestInstance(PER_METHOD) or disable parallel\s
86-
execution altogether. Note that when recording events in parallel, one might see events\s
87-
published by other tests as the application context can be common.
88-
""");
80+
.isEqualTo("""
81+
Test classes or @Nested test classes that @RecordApplicationEvents must not be run \
82+
in parallel with the @TestInstance(PER_CLASS) lifecycle mode. Configure either \
83+
@Execution(SAME_THREAD) or @TestInstance(PER_METHOD) semantics, or disable parallel \
84+
execution altogether. Note that when recording events in parallel, one might see events \
85+
published by other tests since the application context may be shared.""");
8986
}
9087

9188
@Test
92-
void executeTestsInParallelInstancePerMethod() {
89+
void executeTestsInParallelWithInstancePerMethod() {
9390
Class<?> testClass = TestInstancePerMethodTestCase.class;
9491
Events testEvents = EngineTestKit.engine("junit-jupiter")//
9592
.selectors(selectClass(testClass))//
9693
.configurationParameter("junit.jupiter.execution.parallel.enabled", "true")//
9794
.configurationParameter("junit.jupiter.execution.parallel.config.dynamic.factor", "10")//
9895
.execute()//
9996
.testEvents();
100-
//list failed events in case of test errors to get a sense of which tests failed
97+
// list failed events in case of test errors to get a sense of which tests failed
10198
Events failedTests = testEvents.failed();
10299
if (failedTests.count() > 0) {
103100
failedTests.debug();
@@ -141,7 +138,6 @@ void resetPayloads() {
141138

142139
@SpringJUnitConfig
143140
@RecordApplicationEvents
144-
@Execution(ExecutionMode.CONCURRENT)
145141
@TestInstance(Lifecycle.PER_METHOD)
146142
static class TestInstancePerMethodTestCase {
147143

@@ -211,7 +207,7 @@ void compareToApplicationEventsHolder(ApplicationEvents applicationEvents) {
211207

212208
@Test
213209
void asyncPublication(ApplicationEvents events) throws InterruptedException {
214-
final ExecutorService executorService = Executors.newSingleThreadExecutor();
210+
ExecutorService executorService = Executors.newSingleThreadExecutor();
215211
executorService.execute(() -> this.context.publishEvent("asyncPublication"));
216212
executorService.shutdown();
217213
executorService.awaitTermination(10, TimeUnit.SECONDS);

0 commit comments

Comments
 (0)