Skip to content

Commit 655097a

Browse files
committed
Merge pull request #1144 from ttddyy/SPR-14614
* ttddyy-SPR-14614: Revise @DisabledIf support for JUnit Jupiter Introduce @DisabledIf annotation for JUnit 5
2 parents eb193ae + 1936909 commit 655097a

File tree

5 files changed

+426
-2
lines changed

5 files changed

+426
-2
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
27+
import org.springframework.core.annotation.AliasFor;
28+
29+
/**
30+
* {@code @DisabledIf} is used to signal that the annotated test class or test
31+
* method is <em>disabled</em> and should not be executed if the supplied
32+
* {@link #expression} evaluates to {@code true}.
33+
*
34+
* <p>When applied at the class level, all test methods within that class
35+
* are automatically disabled as well.
36+
*
37+
* <p>For basic examples, see the Javadoc for {@link #expression}.
38+
*
39+
* <p>This annotation may be used as a <em>meta-annotation</em> to create
40+
* custom <em>composed annotations</em>. For example, a custom
41+
* {@code @DisabledOnMac} annotation can be created as follows.
42+
*
43+
* <pre style="code">
44+
* {@literal @}Target({ ElementType.TYPE, ElementType.METHOD })
45+
* {@literal @}Retention(RetentionPolicy.RUNTIME)
46+
* {@literal @}DisabledIf(
47+
* expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
48+
* reason = "Disabled on Mac OS"
49+
* )
50+
* public {@literal @}interface DisabledOnMac {}
51+
* </pre>
52+
*
53+
* @author Sam Brannen
54+
* @author Tadaya Tsuyukubo
55+
* @since 5.0
56+
* @see SpringExtension
57+
* @see org.junit.jupiter.api.Disabled
58+
*/
59+
@Target({ ElementType.TYPE, ElementType.METHOD })
60+
@Retention(RetentionPolicy.RUNTIME)
61+
@Documented
62+
@ExtendWith(DisabledIfCondition.class)
63+
public @interface DisabledIf {
64+
65+
/**
66+
* Alias for {@link #expression}; only intended to be used if an
67+
* explicit {@link #reason} is not provided.
68+
*
69+
* @see #expression
70+
*/
71+
@AliasFor("expression")
72+
String value() default "";
73+
74+
/**
75+
* The expression that will be evaluated to determine if the annotated test
76+
* class or test method is <em>disabled</em>.
77+
*
78+
* <p>If the expression evaluates to {@link Boolean#TRUE} or a {@link String}
79+
* equal to {@code "true"} (ignoring case), the test will be disabled.
80+
*
81+
* <p>Expressions can be any of the following.
82+
*
83+
* <ul>
84+
* <li>Spring Expression Language (SpEL) expression &mdash; for example:
85+
* <pre style="code">@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")</pre>
86+
* <li>Placeholder for a property available in the Spring
87+
* {@link org.springframework.core.env.Environment Environment} &mdash; for example:
88+
* <pre style="code">@DisabledIf("${smoke.tests.enabled}")</pre>
89+
* <li>Text literal &mdash; for example:
90+
* <pre style="code">@DisabledIf("true")</pre>
91+
* </ul>
92+
*
93+
* <p>Note, however, that a <em>text literal</em> which is not the result of
94+
* dynamic resolution of a property placeholder is of zero practical value
95+
* since {@code @DisabledIf("true")} is equivalent to {@code @Disabled}
96+
* and {@code @DisabledIf("false")} is logically meaningless.
97+
*
98+
* @see #reason
99+
* @see #value
100+
*/
101+
@AliasFor("value")
102+
String expression() default "";
103+
104+
/**
105+
* The reason this test is disabled.
106+
*
107+
* @see #expression
108+
*/
109+
String reason() default "";
110+
111+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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.lang.annotation.Annotation;
20+
import java.lang.reflect.AnnotatedElement;
21+
import java.util.Optional;
22+
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
25+
26+
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
27+
import org.junit.jupiter.api.extension.ContainerExecutionCondition;
28+
import org.junit.jupiter.api.extension.ContainerExtensionContext;
29+
import org.junit.jupiter.api.extension.ExtensionContext;
30+
import org.junit.jupiter.api.extension.TestExecutionCondition;
31+
import org.junit.jupiter.api.extension.TestExtensionContext;
32+
33+
import org.springframework.beans.factory.config.BeanExpressionContext;
34+
import org.springframework.beans.factory.config.BeanExpressionResolver;
35+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
36+
import org.springframework.context.ApplicationContext;
37+
import org.springframework.context.ConfigurableApplicationContext;
38+
import org.springframework.core.annotation.AnnotatedElementUtils;
39+
import org.springframework.util.Assert;
40+
import org.springframework.util.StringUtils;
41+
42+
/**
43+
* {@code DisabledIfCondition} is a composite {@link ContainerExecutionCondition}
44+
* and {@link TestExecutionCondition} that supports the {@link DisabledIf @DisabledIf}
45+
* annotation when using the <em>Spring TestContext Framework</em> in conjunction
46+
* with JUnit 5's <em>Jupiter</em> programming model.
47+
*
48+
* <p>Any attempt to use {@code DisabledIfCondition} without the presence of
49+
* {@link DisabledIf @DisabledIf} will result in an {@link IllegalStateException}.
50+
*
51+
* @author Sam Brannen
52+
* @author Tadaya Tsuyukubo
53+
* @since 5.0
54+
* @see org.springframework.test.context.junit.jupiter.DisabledIf
55+
* @see org.springframework.test.context.junit.jupiter.SpringExtension
56+
*/
57+
public class DisabledIfCondition implements ContainerExecutionCondition, TestExecutionCondition {
58+
59+
private static final Log logger = LogFactory.getLog(DisabledIfCondition.class);
60+
61+
62+
/**
63+
* Containers are disabled if {@code @DisabledIf} is present on the test class
64+
* and the configured expression evaluates to {@code true}.
65+
*/
66+
@Override
67+
public ConditionEvaluationResult evaluate(ContainerExtensionContext context) {
68+
return evaluateDisabledIf(context);
69+
}
70+
71+
/**
72+
* Tests are disabled if {@code @DisabledIf} is present on the test method
73+
* and the configured expression evaluates to {@code true}.
74+
*/
75+
@Override
76+
public ConditionEvaluationResult evaluate(TestExtensionContext context) {
77+
return evaluateDisabledIf(context);
78+
}
79+
80+
private ConditionEvaluationResult evaluateDisabledIf(ExtensionContext extensionContext) {
81+
AnnotatedElement element = extensionContext.getElement().get();
82+
Optional<DisabledIf> disabledIf = findMergedAnnotation(element, DisabledIf.class);
83+
Assert.state(disabledIf.isPresent(), () -> "@DisabledIf must be present on " + element);
84+
85+
String expression = disabledIf.get().expression().trim();
86+
87+
if (isDisabled(expression, extensionContext)) {
88+
String reason = disabledIf.map(DisabledIf::reason).filter(StringUtils::hasText).orElseGet(
89+
() -> String.format("%s is disabled because @DisabledIf(\"%s\") evaluated to true", element,
90+
expression));
91+
logger.info(reason);
92+
return ConditionEvaluationResult.disabled(reason);
93+
}
94+
else {
95+
String reason = String.format("%s is enabled because @DisabledIf(\"%s\") did not evaluate to true",
96+
element, expression);
97+
logger.debug(reason);
98+
return ConditionEvaluationResult.enabled(reason);
99+
}
100+
}
101+
102+
private boolean isDisabled(String expression, ExtensionContext extensionContext) {
103+
ApplicationContext applicationContext = SpringExtension.getApplicationContext(extensionContext);
104+
105+
if (!(applicationContext instanceof ConfigurableApplicationContext)) {
106+
if (logger.isWarnEnabled()) {
107+
String contextType = (applicationContext != null ? applicationContext.getClass().getName() : "null");
108+
logger.warn(String.format("@DisabledIf(\"%s\") could not be evaluated on [%s] since the test " +
109+
"ApplicationContext [%s] is not a ConfigurableApplicationContext",
110+
expression, extensionContext.getElement(), contextType));
111+
}
112+
return false;
113+
}
114+
115+
ConfigurableBeanFactory configurableBeanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
116+
BeanExpressionResolver expressionResolver = configurableBeanFactory.getBeanExpressionResolver();
117+
BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableBeanFactory, null);
118+
119+
Object result = expressionResolver.evaluate(configurableBeanFactory.resolveEmbeddedValue(expression),
120+
beanExpressionContext);
121+
122+
Assert.state((result instanceof Boolean || result instanceof String), () ->
123+
String.format("@DisabledIf(\"%s\") must evaluate to a String or a Boolean, not %s", expression,
124+
(result != null ? result.getClass().getName() : "null")));
125+
126+
boolean disabled = (result instanceof Boolean && ((Boolean) result).booleanValue()) ||
127+
(result instanceof String && Boolean.parseBoolean((String) result));
128+
129+
return disabled;
130+
}
131+
132+
private static <A extends Annotation> Optional<A> findMergedAnnotation(AnnotatedElement element,
133+
Class<A> annotationType) {
134+
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType));
135+
}
136+
137+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
*
5252
* @author Sam Brannen
5353
* @since 5.0
54+
* @see org.springframework.test.context.junit.jupiter.DisabledIf
5455
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig
5556
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
5657
* @see org.springframework.test.context.TestContextManager
@@ -65,6 +66,7 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes
6566
*/
6667
private static final Namespace namespace = Namespace.create(SpringExtension.class);
6768

69+
6870
/**
6971
* Delegates to {@link TestContextManager#beforeTestClass}.
7072
*/
@@ -184,7 +186,7 @@ public Object resolve(ParameterContext parameterContext, ExtensionContext extens
184186
* application context
185187
* @see org.springframework.test.context.TestContext#getApplicationContext()
186188
*/
187-
private ApplicationContext getApplicationContext(ExtensionContext context) {
189+
static ApplicationContext getApplicationContext(ExtensionContext context) {
188190
return getTestContextManager(context).getTestContext().getApplicationContext();
189191
}
190192

@@ -193,7 +195,7 @@ private ApplicationContext getApplicationContext(ExtensionContext context) {
193195
* {@code ExtensionContext}.
194196
* @return the {@code TestContextManager}; never {@code null}
195197
*/
196-
private TestContextManager getTestContextManager(ExtensionContext context) {
198+
private static TestContextManager getTestContextManager(ExtensionContext context) {
197199
Assert.notNull(context, "ExtensionContext must not be null");
198200
Class<?> testClass = context.getTestClass().get();
199201
Store store = context.getStore(namespace);

0 commit comments

Comments
 (0)