Skip to content

Commit c03f6c6

Browse files
ttddyysbrannen
authored andcommitted
Introduce @DisabledIf annotation for JUnit 5
This commit introduces @DisabledIf annotation that takes SpEL as a condition. The condition is evaluated at run time whether to disable JUnit 5 (Jupiter) test method/class. Issue: SPR-14614
1 parent eb193ae commit c03f6c6

File tree

3 files changed

+257
-1
lines changed

3 files changed

+257
-1
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 org.springframework.core.annotation.AliasFor;
20+
21+
import java.lang.annotation.Documented;
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
/**
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+
*
31+
* @author Tadaya Tsuyukubo
32+
* @since 5.0
33+
* @see SpringExtension
34+
*/
35+
@Target({ ElementType.TYPE, ElementType.METHOD })
36+
@Retention(RetentionPolicy.RUNTIME)
37+
@Documented
38+
public @interface DisabledIf {
39+
40+
/**
41+
* Alias for {@link #condition()}.
42+
*/
43+
@AliasFor("condition")
44+
String value() default "";
45+
46+
/**
47+
* Condition to disable test.
48+
*
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.
52+
*/
53+
@AliasFor("value")
54+
String condition() default "";
55+
56+
String reason() default "";
57+
58+
}

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,39 @@
1616

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

19+
import java.lang.reflect.AnnotatedElement;
1920
import java.lang.reflect.Constructor;
2021
import java.lang.reflect.Executable;
2122
import java.lang.reflect.Method;
2223
import java.lang.reflect.Parameter;
24+
import java.util.Optional;
2325

26+
import org.apache.commons.logging.Log;
27+
import org.apache.commons.logging.LogFactory;
2428
import org.junit.jupiter.api.extension.AfterAllCallback;
2529
import org.junit.jupiter.api.extension.AfterEachCallback;
2630
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
2731
import org.junit.jupiter.api.extension.BeforeAllCallback;
2832
import org.junit.jupiter.api.extension.BeforeEachCallback;
2933
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
34+
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
35+
import org.junit.jupiter.api.extension.ContainerExecutionCondition;
3036
import org.junit.jupiter.api.extension.ContainerExtensionContext;
3137
import org.junit.jupiter.api.extension.ExtensionContext;
3238
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
3339
import org.junit.jupiter.api.extension.ExtensionContext.Store;
3440
import org.junit.jupiter.api.extension.ParameterContext;
3541
import org.junit.jupiter.api.extension.ParameterResolver;
42+
import org.junit.jupiter.api.extension.TestExecutionCondition;
3643
import org.junit.jupiter.api.extension.TestExtensionContext;
3744
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
3845

3946
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;
4050
import org.springframework.context.ApplicationContext;
51+
import org.springframework.context.ConfigurableApplicationContext;
4152
import org.springframework.core.annotation.AnnotatedElementUtils;
4253
import org.springframework.test.context.TestContextManager;
4354
import org.springframework.util.Assert;
@@ -50,21 +61,30 @@
5061
* {@code @ExtendWith(SpringExtension.class)}.
5162
*
5263
* @author Sam Brannen
64+
* @author Tadaya Tsuyukubo
5365
* @since 5.0
5466
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig
5567
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
5668
* @see org.springframework.test.context.TestContextManager
69+
* @see DisabledIf
5770
*/
5871
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
5972
BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback,
60-
ParameterResolver {
73+
ParameterResolver,
74+
ContainerExecutionCondition, TestExecutionCondition {
6175

6276
/**
6377
* {@link Namespace} in which {@code TestContextManagers} are stored, keyed
6478
* by test class.
6579
*/
6680
private static final Namespace namespace = Namespace.create(SpringExtension.class);
6781

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+
87+
6888
/**
6989
* Delegates to {@link TestContextManager#beforeTestClass}.
7090
*/
@@ -175,6 +195,61 @@ public Object resolve(ParameterContext parameterContext, ExtensionContext extens
175195
return ParameterAutowireUtils.resolveDependency(parameter, testClass, applicationContext);
176196
}
177197

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+
178253
/**
179254
* Get the {@link ApplicationContext} associated with the supplied
180255
* {@code ExtensionContext}.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 org.junit.jupiter.api.Nested;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.api.extension.ExtendWith;
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.test.context.ContextConfiguration;
25+
import org.springframework.test.context.TestPropertySource;
26+
27+
import static org.junit.jupiter.api.Assertions.fail;
28+
29+
/**
30+
* Integration tests which demonstrate usage of {@link DisabledIf @DisabledIf}
31+
* enabled by {@link SpringExtension} in a JUnit 5 (Jupiter) environment.
32+
*
33+
* @author Tadaya Tsuyukubo
34+
* @since 5.0
35+
* @see DisabledIf
36+
* @see SpringExtension
37+
*/
38+
class DisabledIfTestCase {
39+
40+
@ExtendWith(SpringExtension.class)
41+
@ContextConfiguration(classes = Config.class)
42+
@TestPropertySource(properties = "foo = true")
43+
@Nested
44+
class DisabledIfOnMethodTestCase {
45+
46+
@Test
47+
@DisabledIf("true")
48+
void disabledByStringTrue() {
49+
fail("This test must be disabled");
50+
}
51+
52+
@Test
53+
@DisabledIf("TrUe")
54+
void disabledByStringTrueIgnoreCase() {
55+
fail("This test must be disabled");
56+
}
57+
58+
@Test
59+
@DisabledIf("${foo}")
60+
void disabledByPropertyPlaceholder() {
61+
fail("This test must be disabled");
62+
}
63+
64+
@Test
65+
@DisabledIf("#{T(java.lang.Boolean).TRUE}")
66+
void disabledBySpelBoolean() {
67+
fail("This test must be disabled");
68+
}
69+
70+
@Test
71+
@DisabledIf("#{'tr' + 'ue'}")
72+
void disabledBySpelStringConcatenation() {
73+
fail("This test must be disabled");
74+
}
75+
76+
@Test
77+
@DisabledIf("#{@booleanTrueBean}")
78+
void disabledBySpelBooleanTrueBean() {
79+
fail("This test must be disabled");
80+
}
81+
82+
@Test
83+
@DisabledIf("#{@stringTrueBean}")
84+
void disabledBySpelStringTrueBean() {
85+
fail("This test must be disabled");
86+
}
87+
88+
}
89+
90+
@ExtendWith(SpringExtension.class)
91+
@ContextConfiguration(classes = Config.class)
92+
@Nested
93+
@DisabledIf("true")
94+
class DisabledIfOnClassTestCase {
95+
96+
@Test
97+
void foo() {
98+
fail("This test must be disabled");
99+
}
100+
101+
// Even though method level condition is not disabling test, class level condition should take precedence
102+
@Test
103+
@DisabledIf("false")
104+
void bar() {
105+
fail("This test must be disabled");
106+
}
107+
108+
}
109+
110+
@Configuration
111+
static class Config {
112+
@Bean
113+
Boolean booleanTrueBean() {
114+
return Boolean.TRUE;
115+
}
116+
117+
@Bean
118+
String stringTrueBean() {
119+
return "true";
120+
}
121+
}
122+
123+
}

0 commit comments

Comments
 (0)