diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java index 8c21fe375be..22a9cfa8e33 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.HierarchyMode; +import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -105,6 +108,7 @@ abstract class AbstractExpressionEvaluatingCondition implements ExecutionConditi boolean loadContext = loadContextExtractor.apply(annotation.get()); boolean evaluatedToTrue = evaluateExpression(expression, loadContext, annotationType, context); + ConditionEvaluationResult result; if (evaluatedToTrue) { String adjective = (enabledOnTrue ? "enabled" : "disabled"); @@ -114,7 +118,7 @@ abstract class AbstractExpressionEvaluatingCondition implements ExecutionConditi if (logger.isInfoEnabled()) { logger.info(reason); } - return (enabledOnTrue ? ConditionEvaluationResult.enabled(reason) + result = (enabledOnTrue ? ConditionEvaluationResult.enabled(reason) : ConditionEvaluationResult.disabled(reason)); } else { @@ -124,9 +128,26 @@ abstract class AbstractExpressionEvaluatingCondition implements ExecutionConditi if (logger.isDebugEnabled()) { logger.debug(reason); } - return (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) : + result = (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) : ConditionEvaluationResult.enabled(reason)); } + + // If we eagerly loaded the ApplicationContext to evaluate SpEL expressions + // and the test class ends up being disabled, we have to check if the + // user asked for the ApplicationContext to be closed via @DirtiesContext, + // since the DirtiesContextTestExecutionListener will never be invoked for + // a disabled test class. + // See https://github.com/spring-projects/spring-framework/issues/26694 + if (loadContext && result.isDisabled() && element instanceof Class) { + Class testClass = (Class) element; + DirtiesContext dirtiesContext = TestContextAnnotationUtils.findMergedAnnotation(testClass, DirtiesContext.class); + if (dirtiesContext != null) { + HierarchyMode hierarchyMode = dirtiesContext.hierarchyMode(); + SpringExtension.getTestContextManager(context).getTestContext().markApplicationContextDirty(hierarchyMode); + } + } + + return result; } private boolean evaluateExpression(String expression, boolean loadContext, diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index 83030f37b22..dfd7aaaa28c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -287,7 +287,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes * Get the {@link TestContextManager} associated with the supplied {@code ExtensionContext}. * @return the {@code TestContextManager} (never {@code null}) */ - private static TestContextManager getTestContextManager(ExtensionContext context) { + static TestContextManager getTestContextManager(ExtensionContext context) { Assert.notNull(context, "ExtensionContext must not be null"); Class testClass = context.getRequiredTestClass(); Store store = getStore(context); diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledIfAndDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledIfAndDirtiesContextTests.java new file mode 100644 index 00000000000..007ab98660f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledIfAndDirtiesContextTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.testkit.engine.EngineTestKit; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * Integration tests which verify support for {@link DisabledIf @DisabledIf} in + * conjunction with {@link DirtiesContext @DirtiesContext} and the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.2.14 + * @see EnabledIfAndDirtiesContextTests + */ +class DisabledIfAndDirtiesContextTests { + + private static AtomicBoolean contextClosed = new AtomicBoolean(); + + + @BeforeEach + void reset() { + contextClosed.set(false); + } + + @Test + void contextShouldBeClosedForEnabledTestClass() { + assertThat(contextClosed).as("context closed").isFalse(); + EngineTestKit.engine("junit-jupiter").selectors( + selectClass(EnabledAndDirtiesContextTestCase.class))// + .execute()// + .testEvents()// + .assertStatistics(stats -> stats.started(1).succeeded(1).failed(0)); + assertThat(contextClosed).as("context closed").isTrue(); + } + + @Test + void contextShouldBeClosedForDisabledTestClass() { + assertThat(contextClosed).as("context closed").isFalse(); + EngineTestKit.engine("junit-jupiter").selectors( + selectClass(DisabledAndDirtiesContextTestCase.class))// + .execute()// + .testEvents()// + .assertStatistics(stats -> stats.started(0).succeeded(0).failed(0)); + assertThat(contextClosed).as("context closed").isTrue(); + } + + + @SpringJUnitConfig(Config.class) + @DisabledIf(expression = "false", loadContext = true) + @DirtiesContext + static class EnabledAndDirtiesContextTestCase { + + @Test + void test() { + /* no-op */ + } + } + + @SpringJUnitConfig(Config.class) + @DisabledIf(expression = "true", loadContext = true) + @DirtiesContext + static class DisabledAndDirtiesContextTestCase { + + @Test + void test() { + fail("This test must be disabled"); + } + } + + @Configuration + static class Config { + + @Bean + DisposableBean disposableBean() { + return () -> contextClosed.set(true); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/EnabledIfAndDirtiesContextTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/EnabledIfAndDirtiesContextTests.java new file mode 100644 index 00000000000..1cbe48b8a60 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/EnabledIfAndDirtiesContextTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context.junit.jupiter; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.platform.testkit.engine.EngineTestKit; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; + +/** + * Integration tests which verify support for {@link EnabledIf @EnabledIf} in + * conjunction with {@link DirtiesContext @DirtiesContext} and the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.2.14 + * @see DisabledIfAndDirtiesContextTests + */ +class EnabledIfAndDirtiesContextTests { + + private static AtomicBoolean contextClosed = new AtomicBoolean(); + + + @BeforeEach + void reset() { + contextClosed.set(false); + } + + @Test + void contextShouldBeClosedForEnabledTestClass() { + assertThat(contextClosed).as("context closed").isFalse(); + EngineTestKit.engine("junit-jupiter").selectors( + selectClass(EnabledAndDirtiesContextTestCase.class))// + .execute()// + .testEvents()// + .assertStatistics(stats -> stats.started(1).succeeded(1).failed(0)); + assertThat(contextClosed).as("context closed").isTrue(); + } + + @Test + void contextShouldBeClosedForDisabledTestClass() { + assertThat(contextClosed).as("context closed").isFalse(); + EngineTestKit.engine("junit-jupiter").selectors( + selectClass(DisabledAndDirtiesContextTestCase.class))// + .execute()// + .testEvents()// + .assertStatistics(stats -> stats.started(0).succeeded(0).failed(0)); + assertThat(contextClosed).as("context closed").isTrue(); + } + + + @SpringJUnitConfig(Config.class) + @EnabledIf(expression = "true", loadContext = true) + @DirtiesContext + static class EnabledAndDirtiesContextTestCase { + + @Test + void test() { + /* no-op */ + } + } + + @SpringJUnitConfig(Config.class) + @EnabledIf(expression = "false", loadContext = true) + @DirtiesContext + static class DisabledAndDirtiesContextTestCase { + + @Test + void test() { + fail("This test must be disabled"); + } + } + + @Configuration + static class Config { + + @Bean + DisposableBean disposableBean() { + return () -> contextClosed.set(true); + } + } + +} diff --git a/spring-test/src/test/resources/log4j2-test.xml b/spring-test/src/test/resources/log4j2-test.xml index 3fb0b802d6a..89d254091a4 100644 --- a/spring-test/src/test/resources/log4j2-test.xml +++ b/spring-test/src/test/resources/log4j2-test.xml @@ -25,6 +25,7 @@ +