Skip to content

Introduce @DisabledIf annotation for JUnit 5 #1144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p> When case insensitive {@code String} "true" or {@code Boolean#TRUE} is returned,
* annotated test method or class is disabled.
* <p> SpEL expression can be used.
*/
@AliasFor("value")
String condition() default "";

String reason() default "";

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,21 +61,30 @@
* {@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
* by test class.
*/
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}.
*/
Expand Down Expand Up @@ -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<AnnotatedElement> 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}.
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
}

}