diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java new file mode 100644 index 000000000000..eefbd43bfc06 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2016 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 + * + * http://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 org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Disable JUnit 5(Jupiter) tests when evaluated condition returns "true" + * that can be either case insensitive {@code String} or {@code Boolean#TRUE}. + * + * @author Tadaya Tsuyukubo + * @since 5.0 + * @see SpringExtension + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DisabledIf { + + /** + * Alias for {@link #condition()}. + */ + @AliasFor("condition") + String value() default ""; + + /** + * Condition to disable test. + * + *

When case insensitive {@code String} "true" or {@code Boolean#TRUE} is returned, + * annotated test method or class is disabled. + *

SpEL expression can be used. + */ + @AliasFor("value") + String condition() default ""; + + String reason() default ""; + +} 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 8eef966a330f..d3a52c90c396 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 @@ -16,28 +16,39 @@ package org.springframework.test.context.junit.jupiter; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.util.Optional; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ContainerExecutionCondition; import org.junit.jupiter.api.extension.ContainerExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestExecutionCondition; import org.junit.jupiter.api.extension.TestExtensionContext; import org.junit.jupiter.api.extension.TestInstancePostProcessor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.context.TestContextManager; import org.springframework.util.Assert; @@ -50,14 +61,17 @@ * {@code @ExtendWith(SpringExtension.class)}. * * @author Sam Brannen + * @author Tadaya Tsuyukubo * @since 5.0 * @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig * @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig * @see org.springframework.test.context.TestContextManager + * @see DisabledIf */ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor, BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, - ParameterResolver { + ParameterResolver, + ContainerExecutionCondition, TestExecutionCondition { /** * {@link Namespace} in which {@code TestContextManagers} are stored, keyed @@ -65,6 +79,12 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes */ private static final Namespace namespace = Namespace.create(SpringExtension.class); + private static final ConditionEvaluationResult TEST_ENABLED = ConditionEvaluationResult.enabled( + "@DisabledIf condition didn't match"); + + private static final Log logger = LogFactory.getLog(SpringExtension.class); + + /** * Delegates to {@link TestContextManager#beforeTestClass}. */ @@ -175,6 +195,61 @@ public Object resolve(ParameterContext parameterContext, ExtensionContext extens return ParameterAutowireUtils.resolveDependency(parameter, testClass, applicationContext); } + @Override + public ConditionEvaluationResult evaluate(ContainerExtensionContext context) { + return evaluateDisabledIf(context); + } + + @Override + public ConditionEvaluationResult evaluate(TestExtensionContext context) { + return evaluateDisabledIf(context); + } + + private ConditionEvaluationResult evaluateDisabledIf(ExtensionContext extensionContext) { + Optional element = extensionContext.getElement(); + if (!element.isPresent()) { + return TEST_ENABLED; + } + + DisabledIf disabledIf = AnnotatedElementUtils.findMergedAnnotation(element.get(), DisabledIf.class); + if (disabledIf == null) { + return TEST_ENABLED; + } + + String condition = disabledIf.condition(); + if (condition.trim().length() == 0) { + return TEST_ENABLED; + } + + ApplicationContext applicationContext = getApplicationContext(extensionContext); + if (!(applicationContext instanceof ConfigurableApplicationContext)) { + return TEST_ENABLED; + } + + ConfigurableBeanFactory configurableBeanFactory = ((ConfigurableApplicationContext) applicationContext) + .getBeanFactory(); + BeanExpressionResolver expressionResolver = configurableBeanFactory.getBeanExpressionResolver(); + BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableBeanFactory, null); + + Object result = expressionResolver + .evaluate(configurableBeanFactory.resolveEmbeddedValue(condition), beanExpressionContext); + + if (result == null || !Boolean.valueOf(result.toString())) { + return TEST_ENABLED; + } + + String reason = disabledIf.reason(); + if (reason.trim().length() == 0) { + String testTarget = extensionContext.getTestMethod().map(Method::getName) + .orElseGet(() -> extensionContext.getTestClass().get().getSimpleName()); + reason = String.format("%s is disabled. condition=%s", testTarget, condition); + } + + logger.info(String.format("%s is disabled. reason=%s", element.get(), reason)); + return ConditionEvaluationResult.disabled(reason); + + } + /** * Get the {@link ApplicationContext} associated with the supplied * {@code ExtensionContext}. diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledIfTestCase.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledIfTestCase.java new file mode 100644 index 000000000000..21c11178db1a --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledIfTestCase.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2016 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 + * + * http://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 org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Integration tests which demonstrate usage of {@link DisabledIf @DisabledIf} + * enabled by {@link SpringExtension} in a JUnit 5 (Jupiter) environment. + * + * @author Tadaya Tsuyukubo + * @since 5.0 + * @see DisabledIf + * @see SpringExtension + */ +class DisabledIfTestCase { + + @ExtendWith(SpringExtension.class) + @ContextConfiguration(classes = Config.class) + @TestPropertySource(properties = "foo = true") + @Nested + class DisabledIfOnMethodTestCase { + + @Test + @DisabledIf("true") + void disabledByStringTrue() { + fail("This test must be disabled"); + } + + @Test + @DisabledIf("TrUe") + void disabledByStringTrueIgnoreCase() { + fail("This test must be disabled"); + } + + @Test + @DisabledIf("${foo}") + void disabledByPropertyPlaceholder() { + fail("This test must be disabled"); + } + + @Test + @DisabledIf("#{T(java.lang.Boolean).TRUE}") + void disabledBySpelBoolean() { + fail("This test must be disabled"); + } + + @Test + @DisabledIf("#{'tr' + 'ue'}") + void disabledBySpelStringConcatenation() { + fail("This test must be disabled"); + } + + @Test + @DisabledIf("#{@booleanTrueBean}") + void disabledBySpelBooleanTrueBean() { + fail("This test must be disabled"); + } + + @Test + @DisabledIf("#{@stringTrueBean}") + void disabledBySpelStringTrueBean() { + fail("This test must be disabled"); + } + + } + + @ExtendWith(SpringExtension.class) + @ContextConfiguration(classes = Config.class) + @Nested + @DisabledIf("true") + class DisabledIfOnClassTestCase { + + @Test + void foo() { + fail("This test must be disabled"); + } + + // Even though method level condition is not disabling test, class level condition should take precedence + @Test + @DisabledIf("false") + void bar() { + fail("This test must be disabled"); + } + + } + + @Configuration + static class Config { + @Bean + Boolean booleanTrueBean() { + return Boolean.TRUE; + } + + @Bean + String stringTrueBean() { + return "true"; + } + } + +}