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 00000000000..e5da0195723 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIf.java @@ -0,0 +1,111 @@ +/* + * 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 java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.core.annotation.AliasFor; + +/** + * {@code @DisabledIf} is used to signal that the annotated test class or test + * method is disabled and should not be executed if the supplied + * {@link #expression} evaluates to {@code true}. + * + *

When applied at the class level, all test methods within that class + * are automatically disabled as well. + * + *

For basic examples, see the Javadoc for {@link #expression}. + * + *

This annotation may be used as a meta-annotation to create + * custom composed annotations. For example, a custom + * {@code @DisabledOnMac} annotation can be created as follows. + * + *

+ * {@literal @}Target({ ElementType.TYPE, ElementType.METHOD })
+ * {@literal @}Retention(RetentionPolicy.RUNTIME)
+ * {@literal @}DisabledIf(
+ *     expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
+ *     reason = "Disabled on Mac OS"
+ * )
+ * public {@literal @}interface DisabledOnMac {}
+ * 
+ * + * @author Sam Brannen + * @author Tadaya Tsuyukubo + * @since 5.0 + * @see SpringExtension + * @see org.junit.jupiter.api.Disabled + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(DisabledIfCondition.class) +public @interface DisabledIf { + + /** + * Alias for {@link #expression}; only intended to be used if an + * explicit {@link #reason} is not provided. + * + * @see #expression + */ + @AliasFor("expression") + String value() default ""; + + /** + * The expression that will be evaluated to determine if the annotated test + * class or test method is disabled. + * + *

If the expression evaluates to {@link Boolean#TRUE} or a {@link String} + * equal to {@code "true"} (ignoring case), the test will be disabled. + * + *

Expressions can be any of the following. + * + *

+ * + *

Note, however, that a text literal which is not the result of + * dynamic resolution of a property placeholder is of zero practical value + * since {@code @DisabledIf("true")} is equivalent to {@code @Disabled} + * and {@code @DisabledIf("false")} is logically meaningless. + * + * @see #reason + * @see #value + */ + @AliasFor("value") + String expression() default ""; + + /** + * The reason this test is disabled. + * + * @see #expression + */ + String reason() default ""; + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java new file mode 100644 index 00000000000..ada71169241 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/DisabledIfCondition.java @@ -0,0 +1,137 @@ +/* + * 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 java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +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.TestExecutionCondition; +import org.junit.jupiter.api.extension.TestExtensionContext; + +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.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@code DisabledIfCondition} is a composite {@link ContainerExecutionCondition} + * and {@link TestExecutionCondition} that supports the {@link DisabledIf @DisabledIf} + * annotation when using the Spring TestContext Framework in conjunction + * with JUnit 5's Jupiter programming model. + * + *

Any attempt to use {@code DisabledIfCondition} without the presence of + * {@link DisabledIf @DisabledIf} will result in an {@link IllegalStateException}. + * + * @author Sam Brannen + * @author Tadaya Tsuyukubo + * @since 5.0 + * @see org.springframework.test.context.junit.jupiter.DisabledIf + * @see org.springframework.test.context.junit.jupiter.SpringExtension + */ +public class DisabledIfCondition implements ContainerExecutionCondition, TestExecutionCondition { + + private static final Log logger = LogFactory.getLog(DisabledIfCondition.class); + + + /** + * Containers are disabled if {@code @DisabledIf} is present on the test class + * and the configured expression evaluates to {@code true}. + */ + @Override + public ConditionEvaluationResult evaluate(ContainerExtensionContext context) { + return evaluateDisabledIf(context); + } + + /** + * Tests are disabled if {@code @DisabledIf} is present on the test method + * and the configured expression evaluates to {@code true}. + */ + @Override + public ConditionEvaluationResult evaluate(TestExtensionContext context) { + return evaluateDisabledIf(context); + } + + private ConditionEvaluationResult evaluateDisabledIf(ExtensionContext extensionContext) { + AnnotatedElement element = extensionContext.getElement().get(); + Optional disabledIf = findMergedAnnotation(element, DisabledIf.class); + Assert.state(disabledIf.isPresent(), () -> "@DisabledIf must be present on " + element); + + String expression = disabledIf.get().expression().trim(); + + if (isDisabled(expression, extensionContext)) { + String reason = disabledIf.map(DisabledIf::reason).filter(StringUtils::hasText).orElseGet( + () -> String.format("%s is disabled because @DisabledIf(\"%s\") evaluated to true", element, + expression)); + logger.info(reason); + return ConditionEvaluationResult.disabled(reason); + } + else { + String reason = String.format("%s is enabled because @DisabledIf(\"%s\") did not evaluate to true", + element, expression); + logger.debug(reason); + return ConditionEvaluationResult.enabled(reason); + } + } + + private boolean isDisabled(String expression, ExtensionContext extensionContext) { + ApplicationContext applicationContext = SpringExtension.getApplicationContext(extensionContext); + + if (!(applicationContext instanceof ConfigurableApplicationContext)) { + if (logger.isWarnEnabled()) { + String contextType = (applicationContext != null ? applicationContext.getClass().getName() : "null"); + logger.warn(String.format("@DisabledIf(\"%s\") could not be evaluated on [%s] since the test " + + "ApplicationContext [%s] is not a ConfigurableApplicationContext", + expression, extensionContext.getElement(), contextType)); + } + return false; + } + + ConfigurableBeanFactory configurableBeanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); + BeanExpressionResolver expressionResolver = configurableBeanFactory.getBeanExpressionResolver(); + BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableBeanFactory, null); + + Object result = expressionResolver.evaluate(configurableBeanFactory.resolveEmbeddedValue(expression), + beanExpressionContext); + + Assert.state((result instanceof Boolean || result instanceof String), () -> + String.format("@DisabledIf(\"%s\") must evaluate to a String or a Boolean, not %s", expression, + (result != null ? result.getClass().getName() : "null"))); + + boolean disabled = (result instanceof Boolean && ((Boolean) result).booleanValue()) || + (result instanceof String && Boolean.parseBoolean((String) result)); + + return disabled; + } + + private static Optional findMergedAnnotation(AnnotatedElement element, + Class annotationType) { + return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType)); + } + +} 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 8eef966a330..93d81bb1e82 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 @@ -51,6 +51,7 @@ import org.springframework.util.Assert; * * @author Sam Brannen * @since 5.0 + * @see org.springframework.test.context.junit.jupiter.DisabledIf * @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig * @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig * @see org.springframework.test.context.TestContextManager @@ -65,6 +66,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes */ private static final Namespace namespace = Namespace.create(SpringExtension.class); + /** * Delegates to {@link TestContextManager#beforeTestClass}. */ @@ -184,7 +186,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes * application context * @see org.springframework.test.context.TestContext#getApplicationContext() */ - private ApplicationContext getApplicationContext(ExtensionContext context) { + static ApplicationContext getApplicationContext(ExtensionContext context) { return getTestContextManager(context).getTestContext().getApplicationContext(); } @@ -193,7 +195,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes * {@code ExtensionContext}. * @return the {@code TestContextManager}; never {@code null} */ - private TestContextManager getTestContextManager(ExtensionContext context) { + private static TestContextManager getTestContextManager(ExtensionContext context) { Assert.notNull(context, "ExtensionContext must not be null"); Class testClass = context.getTestClass().get(); Store store = context.getStore(namespace); 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 00000000000..152bcbd3001 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledIfTestCase.java @@ -0,0 +1,136 @@ +/* + * 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.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestPropertySource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests which verify support for {@link DisabledIf @DisabledIf} + * in conjunction with the {@link SpringExtension} in a JUnit 5 (Jupiter) + * environment. + * + * @author Tadaya Tsuyukubo + * @author Sam Brannen + * @since 5.0 + * @see DisabledIf + * @see SpringExtension + */ +class DisabledIfTestCase { + + @SpringJUnitConfig(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("#{6 * 7 == 42}") + void disabledBySpelMathematicalComparison() { + fail("This test must be disabled"); + } + + @Test + @DisabledOnMac + void disabledBySpelOsCheckInCustomComposedAnnotation() { + assertFalse(System.getProperty("os.name").contains("Mac"), "This test must be disabled on Mac OS"); + } + + @Test + @DisabledIf("#{@booleanTrueBean}") + void disabledBySpelBooleanTrueBean() { + fail("This test must be disabled"); + } + + @Test + @DisabledIf("#{@stringTrueBean}") + void disabledBySpelStringTrueBean() { + fail("This test must be disabled"); + } + + } + + @SpringJUnitConfig(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"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledOnMac.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledOnMac.java new file mode 100644 index 00000000000..6a62db34012 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/DisabledOnMac.java @@ -0,0 +1,38 @@ +/* + * 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 java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Demo composed annotation for {@link DisabledIf @DisabledIf} that + * disables a test class or test method if the current operating system is + * Mac OS. + * + * @author Sam Brannen + * @since 5.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@DisabledIf(expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", reason = "Disabled on Mac OS") +public @interface DisabledOnMac { +}