Skip to content

Commit 1936909

Browse files
committed
Revise @DisabledIf support for JUnit Jupiter
- Extracted stand-alone DisabledIfCondition from the SpringExtension so that the condition is only evaluated when necessary. - Simplified implementation of DisabledIfCondition. - Overhauled and extended logging in DisabledIfCondition. - DisabledIfCondition now throws an IllegalStateException if @DisabledIf is not present on the test element or if the expression does not evaluate to a String or Boolean. - Each generated ConditionEvaluationResult now includes the actual expression in the default reason. - @DisabledIf is now auto-configured to be evaluated by the DisabledIfCondition since it is now meta-annotated with @ExtendWith(DisabledIfCondition.class) - Overhauled documentation for @DisabledIf and provided standard examples as well as an @DisabledOnMac annotation to demonstrate support for custom composed annotations. Issue: SPR-14614
1 parent c03f6c6 commit 1936909

File tree

5 files changed

+266
-98
lines changed

5 files changed

+266
-98
lines changed

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

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,96 @@
1616

1717
package org.springframework.test.context.junit.jupiter;
1818

19-
import org.springframework.core.annotation.AliasFor;
20-
2119
import java.lang.annotation.Documented;
2220
import java.lang.annotation.ElementType;
2321
import java.lang.annotation.Retention;
2422
import java.lang.annotation.RetentionPolicy;
2523
import java.lang.annotation.Target;
2624

25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
27+
import org.springframework.core.annotation.AliasFor;
28+
2729
/**
28-
* Disable JUnit 5(Jupiter) tests when evaluated condition returns "true"
29-
* that can be either case insensitive {@code String} or {@code Boolean#TRUE}.
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>
3052
*
53+
* @author Sam Brannen
3154
* @author Tadaya Tsuyukubo
3255
* @since 5.0
3356
* @see SpringExtension
57+
* @see org.junit.jupiter.api.Disabled
3458
*/
3559
@Target({ ElementType.TYPE, ElementType.METHOD })
3660
@Retention(RetentionPolicy.RUNTIME)
3761
@Documented
62+
@ExtendWith(DisabledIfCondition.class)
3863
public @interface DisabledIf {
3964

4065
/**
41-
* Alias for {@link #condition()}.
66+
* Alias for {@link #expression}; only intended to be used if an
67+
* explicit {@link #reason} is not provided.
68+
*
69+
* @see #expression
4270
*/
43-
@AliasFor("condition")
71+
@AliasFor("expression")
4472
String value() default "";
4573

4674
/**
47-
* Condition to disable test.
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>
4892
*
49-
* <p> When case insensitive {@code String} "true" or {@code Boolean#TRUE} is returned,
50-
* annotated test method or class is disabled.
51-
* <p> SpEL expression can be used.
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
52100
*/
53101
@AliasFor("value")
54-
String condition() default "";
102+
String expression() default "";
55103

104+
/**
105+
* The reason this test is disabled.
106+
*
107+
* @see #expression
108+
*/
56109
String reason() default "";
57110

58111
}
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 & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,28 @@
1616

1717
package org.springframework.test.context.junit.jupiter;
1818

19-
import java.lang.reflect.AnnotatedElement;
2019
import java.lang.reflect.Constructor;
2120
import java.lang.reflect.Executable;
2221
import java.lang.reflect.Method;
2322
import java.lang.reflect.Parameter;
24-
import java.util.Optional;
2523

26-
import org.apache.commons.logging.Log;
27-
import org.apache.commons.logging.LogFactory;
2824
import org.junit.jupiter.api.extension.AfterAllCallback;
2925
import org.junit.jupiter.api.extension.AfterEachCallback;
3026
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
3127
import org.junit.jupiter.api.extension.BeforeAllCallback;
3228
import org.junit.jupiter.api.extension.BeforeEachCallback;
3329
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
34-
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
35-
import org.junit.jupiter.api.extension.ContainerExecutionCondition;
3630
import org.junit.jupiter.api.extension.ContainerExtensionContext;
3731
import org.junit.jupiter.api.extension.ExtensionContext;
3832
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
3933
import org.junit.jupiter.api.extension.ExtensionContext.Store;
4034
import org.junit.jupiter.api.extension.ParameterContext;
4135
import org.junit.jupiter.api.extension.ParameterResolver;
42-
import org.junit.jupiter.api.extension.TestExecutionCondition;
4336
import org.junit.jupiter.api.extension.TestExtensionContext;
4437
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
4538

4639
import org.springframework.beans.factory.annotation.Autowired;
47-
import org.springframework.beans.factory.config.BeanExpressionContext;
48-
import org.springframework.beans.factory.config.BeanExpressionResolver;
49-
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
5040
import org.springframework.context.ApplicationContext;
51-
import org.springframework.context.ConfigurableApplicationContext;
5241
import org.springframework.core.annotation.AnnotatedElementUtils;
5342
import org.springframework.test.context.TestContextManager;
5443
import org.springframework.util.Assert;
@@ -61,29 +50,22 @@
6150
* {@code @ExtendWith(SpringExtension.class)}.
6251
*
6352
* @author Sam Brannen
64-
* @author Tadaya Tsuyukubo
6553
* @since 5.0
54+
* @see org.springframework.test.context.junit.jupiter.DisabledIf
6655
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig
6756
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
6857
* @see org.springframework.test.context.TestContextManager
69-
* @see DisabledIf
7058
*/
7159
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
7260
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
73-
ParameterResolver,
74-
ContainerExecutionCondition, TestExecutionCondition {
61+
ParameterResolver {
7562

7663
/**
7764
* {@link Namespace} in which {@code TestContextManagers} are stored, keyed
7865
* by test class.
7966
*/
8067
private static final Namespace namespace = Namespace.create(SpringExtension.class);
8168

82-
private static final ConditionEvaluationResult TEST_ENABLED = ConditionEvaluationResult.enabled(
83-
"@DisabledIf condition didn't match");
84-
85-
private static final Log logger = LogFactory.getLog(SpringExtension.class);
86-
8769

8870
/**
8971
* Delegates to {@link TestContextManager#beforeTestClass}.
@@ -195,61 +177,6 @@ public Object resolve(ParameterContext parameterContext, ExtensionContext extens
195177
return ParameterAutowireUtils.resolveDependency(parameter, testClass, applicationContext);
196178
}
197179

198-
@Override
199-
public ConditionEvaluationResult evaluate(ContainerExtensionContext context) {
200-
return evaluateDisabledIf(context);
201-
}
202-
203-
@Override
204-
public ConditionEvaluationResult evaluate(TestExtensionContext context) {
205-
return evaluateDisabledIf(context);
206-
}
207-
208-
private ConditionEvaluationResult evaluateDisabledIf(ExtensionContext extensionContext) {
209-
Optional<AnnotatedElement> element = extensionContext.getElement();
210-
if (!element.isPresent()) {
211-
return TEST_ENABLED;
212-
}
213-
214-
DisabledIf disabledIf = AnnotatedElementUtils.findMergedAnnotation(element.get(), DisabledIf.class);
215-
if (disabledIf == null) {
216-
return TEST_ENABLED;
217-
}
218-
219-
String condition = disabledIf.condition();
220-
if (condition.trim().length() == 0) {
221-
return TEST_ENABLED;
222-
}
223-
224-
ApplicationContext applicationContext = getApplicationContext(extensionContext);
225-
if (!(applicationContext instanceof ConfigurableApplicationContext)) {
226-
return TEST_ENABLED;
227-
}
228-
229-
ConfigurableBeanFactory configurableBeanFactory = ((ConfigurableApplicationContext) applicationContext)
230-
.getBeanFactory();
231-
BeanExpressionResolver expressionResolver = configurableBeanFactory.getBeanExpressionResolver();
232-
BeanExpressionContext beanExpressionContext = new BeanExpressionContext(configurableBeanFactory, null);
233-
234-
Object result = expressionResolver
235-
.evaluate(configurableBeanFactory.resolveEmbeddedValue(condition), beanExpressionContext);
236-
237-
if (result == null || !Boolean.valueOf(result.toString())) {
238-
return TEST_ENABLED;
239-
}
240-
241-
String reason = disabledIf.reason();
242-
if (reason.trim().length() == 0) {
243-
String testTarget = extensionContext.getTestMethod().map(Method::getName)
244-
.orElseGet(() -> extensionContext.getTestClass().get().getSimpleName());
245-
reason = String.format("%s is disabled. condition=%s", testTarget, condition);
246-
}
247-
248-
logger.info(String.format("%s is disabled. reason=%s", element.get(), reason));
249-
return ConditionEvaluationResult.disabled(reason);
250-
251-
}
252-
253180
/**
254181
* Get the {@link ApplicationContext} associated with the supplied
255182
* {@code ExtensionContext}.
@@ -259,7 +186,7 @@ private ConditionEvaluationResult evaluateDisabledIf(ExtensionContext extensionC
259186
* application context
260187
* @see org.springframework.test.context.TestContext#getApplicationContext()
261188
*/
262-
private ApplicationContext getApplicationContext(ExtensionContext context) {
189+
static ApplicationContext getApplicationContext(ExtensionContext context) {
263190
return getTestContextManager(context).getTestContext().getApplicationContext();
264191
}
265192

@@ -268,7 +195,7 @@ private ApplicationContext getApplicationContext(ExtensionContext context) {
268195
* {@code ExtensionContext}.
269196
* @return the {@code TestContextManager}; never {@code null}
270197
*/
271-
private TestContextManager getTestContextManager(ExtensionContext context) {
198+
private static TestContextManager getTestContextManager(ExtensionContext context) {
272199
Assert.notNull(context, "ExtensionContext must not be null");
273200
Class<?> testClass = context.getTestClass().get();
274201
Store store = context.getStore(namespace);

0 commit comments

Comments
 (0)