Skip to content

Commit e4f753e

Browse files
committed
Honor class-level @DirtiesContext if test class is disabled via SpEL
Prior to this commit, if a test class annotated with @DirtiesContext and @EnabledIf/@DisabledIf with `loadContext = true` was disabled due to the evaluated SpEL expression, the ApplicationContext would not be marked as dirty and closed. The reason is that @EnabledIf/@DisabledIf are implemented via JUnit Jupiter's ExecutionCondition extension API which results in the entire test class (as well as any associated extension callbacks) being skipped if the condition evaluates to `disabled`. This effectively prevents any of Spring's TestExecutionListener APIs from being invoked. Consequently, the DirtiesContextTestExecutionListener does not get a chance to honor the class-level @DirtiesContext declaration. This commit fixes this by implementing part of the logic of DirtiesContextTestExecutionListener in AbstractExpressionEvaluatingCondition (i.e., the base class for @EnabledIf/@DisabledIf support). Specifically, if the test class for an eagerly loaded ApplicationContext is disabled, AbstractExpressionEvaluatingCondition will now mark the test ApplicationContext as dirty if the test class is annotated with @DirtiesContext. Closes gh-26694
1 parent 4982b5f commit e4f753e

File tree

5 files changed

+241
-5
lines changed

5 files changed

+241
-5
lines changed

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,6 +34,9 @@
3434
import org.springframework.context.ConfigurableApplicationContext;
3535
import org.springframework.context.support.GenericApplicationContext;
3636
import org.springframework.core.annotation.AnnotatedElementUtils;
37+
import org.springframework.test.annotation.DirtiesContext;
38+
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
39+
import org.springframework.test.context.TestContextAnnotationUtils;
3740
import org.springframework.util.Assert;
3841
import org.springframework.util.StringUtils;
3942

@@ -105,6 +108,7 @@ protected <A extends Annotation> ConditionEvaluationResult evaluateAnnotation(Cl
105108

106109
boolean loadContext = loadContextExtractor.apply(annotation.get());
107110
boolean evaluatedToTrue = evaluateExpression(expression, loadContext, annotationType, context);
111+
ConditionEvaluationResult result;
108112

109113
if (evaluatedToTrue) {
110114
String adjective = (enabledOnTrue ? "enabled" : "disabled");
@@ -114,7 +118,7 @@ protected <A extends Annotation> ConditionEvaluationResult evaluateAnnotation(Cl
114118
if (logger.isInfoEnabled()) {
115119
logger.info(reason);
116120
}
117-
return (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
121+
result = (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
118122
: ConditionEvaluationResult.disabled(reason));
119123
}
120124
else {
@@ -124,9 +128,26 @@ protected <A extends Annotation> ConditionEvaluationResult evaluateAnnotation(Cl
124128
if (logger.isDebugEnabled()) {
125129
logger.debug(reason);
126130
}
127-
return (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) :
131+
result = (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) :
128132
ConditionEvaluationResult.enabled(reason));
129133
}
134+
135+
// If we eagerly loaded the ApplicationContext to evaluate SpEL expressions
136+
// and the test class ends up being disabled, we have to check if the
137+
// user asked for the ApplicationContext to be closed via @DirtiesContext,
138+
// since the DirtiesContextTestExecutionListener will never be invoked for
139+
// a disabled test class.
140+
// See https://github.com/spring-projects/spring-framework/issues/26694
141+
if (loadContext && result.isDisabled() && element instanceof Class) {
142+
Class<?> testClass = (Class<?>) element;
143+
DirtiesContext dirtiesContext = TestContextAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class);
144+
if (dirtiesContext != null) {
145+
HierarchyMode hierarchyMode = dirtiesContext.hierarchyMode();
146+
SpringExtension.getTestContextManager(context).getTestContext().markApplicationContextDirty(hierarchyMode);
147+
}
148+
}
149+
150+
return result;
130151
}
131152

132153
private <A extends Annotation> boolean evaluateExpression(String expression, boolean loadContext,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -287,7 +287,7 @@ public static ApplicationContext getApplicationContext(ExtensionContext context)
287287
* Get the {@link TestContextManager} associated with the supplied {@code ExtensionContext}.
288288
* @return the {@code TestContextManager} (never {@code null})
289289
*/
290-
private static TestContextManager getTestContextManager(ExtensionContext context) {
290+
static TestContextManager getTestContextManager(ExtensionContext context) {
291291
Assert.notNull(context, "ExtensionContext must not be null");
292292
Class<?> testClass = context.getRequiredTestClass();
293293
Store store = getStore(context);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.junit.jupiter;
18+
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.platform.testkit.engine.EngineTestKit;
24+
25+
import org.springframework.beans.factory.DisposableBean;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.test.annotation.DirtiesContext;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.fail;
32+
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
33+
34+
/**
35+
* Integration tests which verify support for {@link DisabledIf @DisabledIf} in
36+
* conjunction with {@link DirtiesContext @DirtiesContext} and the
37+
* {@link SpringExtension} in a JUnit Jupiter environment.
38+
*
39+
* @author Sam Brannen
40+
* @since 5.2.14
41+
* @see EnabledIfAndDirtiesContextTests
42+
*/
43+
class DisabledIfAndDirtiesContextTests {
44+
45+
private static AtomicBoolean contextClosed = new AtomicBoolean();
46+
47+
48+
@BeforeEach
49+
void reset() {
50+
contextClosed.set(false);
51+
}
52+
53+
@Test
54+
void contextShouldBeClosedForEnabledTestClass() {
55+
assertThat(contextClosed).as("context closed").isFalse();
56+
EngineTestKit.engine("junit-jupiter").selectors(
57+
selectClass(EnabledAndDirtiesContextTestCase.class))//
58+
.execute()//
59+
.testEvents()//
60+
.assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
61+
assertThat(contextClosed).as("context closed").isTrue();
62+
}
63+
64+
@Test
65+
void contextShouldBeClosedForDisabledTestClass() {
66+
assertThat(contextClosed).as("context closed").isFalse();
67+
EngineTestKit.engine("junit-jupiter").selectors(
68+
selectClass(DisabledAndDirtiesContextTestCase.class))//
69+
.execute()//
70+
.testEvents()//
71+
.assertStatistics(stats -> stats.started(0).succeeded(0).failed(0));
72+
assertThat(contextClosed).as("context closed").isTrue();
73+
}
74+
75+
76+
@SpringJUnitConfig(Config.class)
77+
@DisabledIf(expression = "false", loadContext = true)
78+
@DirtiesContext
79+
static class EnabledAndDirtiesContextTestCase {
80+
81+
@Test
82+
void test() {
83+
/* no-op */
84+
}
85+
}
86+
87+
@SpringJUnitConfig(Config.class)
88+
@DisabledIf(expression = "true", loadContext = true)
89+
@DirtiesContext
90+
static class DisabledAndDirtiesContextTestCase {
91+
92+
@Test
93+
void test() {
94+
fail("This test must be disabled");
95+
}
96+
}
97+
98+
@Configuration
99+
static class Config {
100+
101+
@Bean
102+
DisposableBean disposableBean() {
103+
return () -> contextClosed.set(true);
104+
}
105+
}
106+
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2002-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.context.junit.jupiter;
18+
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.platform.testkit.engine.EngineTestKit;
24+
25+
import org.springframework.beans.factory.DisposableBean;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.test.annotation.DirtiesContext;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.fail;
32+
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
33+
34+
/**
35+
* Integration tests which verify support for {@link EnabledIf @EnabledIf} in
36+
* conjunction with {@link DirtiesContext @DirtiesContext} and the
37+
* {@link SpringExtension} in a JUnit Jupiter environment.
38+
*
39+
* @author Sam Brannen
40+
* @since 5.2.14
41+
* @see DisabledIfAndDirtiesContextTests
42+
*/
43+
class EnabledIfAndDirtiesContextTests {
44+
45+
private static AtomicBoolean contextClosed = new AtomicBoolean();
46+
47+
48+
@BeforeEach
49+
void reset() {
50+
contextClosed.set(false);
51+
}
52+
53+
@Test
54+
void contextShouldBeClosedForEnabledTestClass() {
55+
assertThat(contextClosed).as("context closed").isFalse();
56+
EngineTestKit.engine("junit-jupiter").selectors(
57+
selectClass(EnabledAndDirtiesContextTestCase.class))//
58+
.execute()//
59+
.testEvents()//
60+
.assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
61+
assertThat(contextClosed).as("context closed").isTrue();
62+
}
63+
64+
@Test
65+
void contextShouldBeClosedForDisabledTestClass() {
66+
assertThat(contextClosed).as("context closed").isFalse();
67+
EngineTestKit.engine("junit-jupiter").selectors(
68+
selectClass(DisabledAndDirtiesContextTestCase.class))//
69+
.execute()//
70+
.testEvents()//
71+
.assertStatistics(stats -> stats.started(0).succeeded(0).failed(0));
72+
assertThat(contextClosed).as("context closed").isTrue();
73+
}
74+
75+
76+
@SpringJUnitConfig(Config.class)
77+
@EnabledIf(expression = "true", loadContext = true)
78+
@DirtiesContext
79+
static class EnabledAndDirtiesContextTestCase {
80+
81+
@Test
82+
void test() {
83+
/* no-op */
84+
}
85+
}
86+
87+
@SpringJUnitConfig(Config.class)
88+
@EnabledIf(expression = "false", loadContext = true)
89+
@DirtiesContext
90+
static class DisabledAndDirtiesContextTestCase {
91+
92+
@Test
93+
void test() {
94+
fail("This test must be disabled");
95+
}
96+
}
97+
98+
@Configuration
99+
static class Config {
100+
101+
@Bean
102+
DisposableBean disposableBean() {
103+
return () -> contextClosed.set(true);
104+
}
105+
}
106+
107+
}

spring-test/src/test/resources/log4j2-test.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<Logger name="org.springframework.test.context.support.DelegatingSmartContextLoader" level="info" />
2626
<Logger name="org.springframework.test.context.support.AbstractGenericContextLoader" level="info" />
2727
<Logger name="org.springframework.test.context.support.AnnotationConfigContextLoader" level="info" />
28+
<Logger name="org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener" level="warn" />
2829
<Logger name="org.springframework.test.context.support.TestPropertySourceUtils" level="trace" />
2930
<Logger name="org.springframework.beans" level="warn" />
3031
<Logger name="org.springframework.test.web.servlet.result" level="debug" />

0 commit comments

Comments
 (0)