/*
* 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.junit4;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.internal.runners.model.ReflectiveCallable;
import org.junit.internal.runners.statements.ExpectException;
import org.junit.internal.runners.statements.Fail;
import org.junit.internal.runners.statements.FailOnTimeout;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.springframework.test.annotation.ProfileValueUtils;
import org.springframework.test.annotation.TestAnnotationUtils;
import org.springframework.test.context.TestContextManager;
import org.springframework.test.context.junit4.rules.SpringClassRule;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks;
import org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks;
import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks;
import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks;
import org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks;
import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks;
import org.springframework.test.context.junit4.statements.SpringFailOnTimeout;
import org.springframework.test.context.junit4.statements.SpringRepeat;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@code SpringJUnit4ClassRunner} is a custom extension of JUnit's
* {@link BlockJUnit4ClassRunner} which provides functionality of the
* <em>Spring TestContext Framework</em> to standard JUnit tests by means of the
* {@link TestContextManager} and associated support classes and annotations.
*
* <p>To use this class, simply annotate a JUnit 4 based test class with
* {@code @RunWith(SpringJUnit4ClassRunner.class)} or {@code @RunWith(SpringRunner.class)}.
*
* <p>The following list constitutes all annotations currently supported directly
* or indirectly by {@code SpringJUnit4ClassRunner}. <em>(Note that additional
* annotations may be supported by various
* {@link org.springframework.test.context.TestExecutionListener TestExecutionListener}
* or {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper}
* implementations.)</em>
*
* <ul>
* <li>{@link Test#expected() @Test(expected=...)}</li>
* <li>{@link Test#timeout() @Test(timeout=...)}</li>
* <li>{@link org.springframework.test.annotation.Timed @Timed}</li>
* <li>{@link org.springframework.test.annotation.Repeat @Repeat}</li>
* <li>{@link Ignore @Ignore}</li>
* <li>{@link org.springframework.test.annotation.ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}</li>
* <li>{@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}</li>
* </ul>
*
* <p>If you would like to use the Spring TestContext Framework with a runner
* other than this one, use {@link SpringClassRule} and {@link SpringMethodRule}.
*
* <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 2.5
* @see SpringRunner
* @see TestContextManager
* @see AbstractJUnit4SpringContextTests
* @see AbstractTransactionalJUnit4SpringContextTests
* @see org.springframework.test.context.junit4.rules.SpringClassRule
* @see org.springframework.test.context.junit4.rules.SpringMethodRule
*/
public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
private static final Log logger = LogFactory.getLog(SpringJUnit4ClassRunner.class);
private static final Method withRulesMethod;
static {
Assert.state(ClassUtils.isPresent("org.junit.internal.Throwables", SpringJUnit4ClassRunner.class.getClassLoader()),
"SpringJUnit4ClassRunner requires JUnit 4.12 or higher.");
withRulesMethod = ReflectionUtils.findMethod(SpringJUnit4ClassRunner.class, "withRules",
FrameworkMethod.class, Object.class, Statement.class);
Assert.state(withRulesMethod != null, "SpringJUnit4ClassRunner requires JUnit 4.12 or higher.");
ReflectionUtils.makeAccessible(withRulesMethod);
}
private final TestContextManager testContextManager;
private static void ensureSpringRulesAreNotPresent(Class<?> testClass) {
for (Field field : testClass.getFields()) {
Assert.state(!SpringClassRule.class.isAssignableFrom(field.getType()), () -> String.format(
"Detected SpringClassRule field in test class [%s], " +
"but SpringClassRule cannot be used with the SpringJUnit4ClassRunner.", testClass.getName()));
Assert.state(!SpringMethodRule.class.isAssignableFrom(field.getType()), () -> String.format(
"Detected SpringMethodRule field in test class [%s], " +
"but SpringMethodRule cannot be used with the SpringJUnit4ClassRunner.", testClass.getName()));
}
}
/**
* Construct a new {@code SpringJUnit4ClassRunner} and initialize a
* {@link TestContextManager} to provide Spring testing functionality to
* standard JUnit tests.
* @param clazz the test class to be run
* @see #createTestContextManager(Class)
*/
public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
super(clazz);
if (logger.isDebugEnabled()) {
logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]");
}
ensureSpringRulesAreNotPresent(clazz);
this.testContextManager = createTestContextManager(clazz);
}
/**
* Create a new {@link TestContextManager} for the supplied test class.
* <p>Can be overridden by subclasses.
* @param clazz the test class to be managed
*/
protected TestContextManager createTestContextManager(Class<?> clazz) {
return new TestContextManager(clazz);
}
/**
* Get the {@link TestContextManager} associated with this runner.
*/
protected final TestContextManager getTestContextManager() {
return this.testContextManager;
}
/**
* Return a description suitable for an ignored test class if the test is
* disabled via {@code @IfProfileValue} at the class-level, and
* otherwise delegate to the parent implementation.
* @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
*/
@Override
public Description getDescription() {
if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
return Description.createSuiteDescription(getTestClass().getJavaClass());
}
return super.getDescription();
}
/**
* Check whether the test is enabled in the current execution environment.
* <p>This prevents classes with a non-matching {@code @IfProfileValue}
* annotation from running altogether, even skipping the execution of
* {@code prepareTestInstance()} methods in {@code TestExecutionListeners}.
* @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
* @see org.springframework.test.annotation.IfProfileValue
* @see org.springframework.test.context.TestExecutionListener
*/
@Override
public void run(RunNotifier notifier) {
if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
notifier.fireTestIgnored(getDescription());
return;
}
super.run(notifier);
}
/**
* Wrap the {@link Statement} returned by the parent implementation with a
* {@code RunBeforeTestClassCallbacks} statement, thus preserving the
* default JUnit functionality while adding support for the Spring TestContext
* Framework.
* @see RunBeforeTestClassCallbacks
*/
@Override
protected Statement withBeforeClasses(Statement statement) {
Statement junitBeforeClasses = super.withBeforeClasses(statement);
return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager());
}
/**
* Wrap the {@link Statement} returned by the parent implementation with a
* {@code RunAfterTestClassCallbacks} statement, thus preserving the default
* JUnit functionality while adding support for the Spring TestContext Framework.
* @see RunAfterTestClassCallbacks
*/
@Override
protected Statement withAfterClasses(Statement statement) {
Statement junitAfterClasses = super.withAfterClasses(statement);
return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager());
}
/**
* Delegate to the parent implementation for creating the test instance and
* then allow the {@link #getTestContextManager() TestContextManager} to
* prepare the test instance before returning it.
* @see TestContextManager#prepareTestInstance
*/
@Override
protected Object createTest() throws Exception {
Object testInstance = super.createTest();
getTestContextManager().prepareTestInstance(testInstance);
return testInstance;
}
/**
* Perform the same logic as
* {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)},
* except that tests are determined to be <em>ignored</em> by
* {@link #isTestMethodIgnored(FrameworkMethod)}.
*/
@Override
protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) {
Description description = describeChild(frameworkMethod);
if (isTestMethodIgnored(frameworkMethod)) {
notifier.fireTestIgnored(description);
}
else {
Statement statement;
try {
statement = methodBlock(frameworkMethod);
}
catch (Throwable ex) {
statement = new Fail(ex);
}
runLeaf(statement, description, notifier);
}
}
/**
* Augment the default JUnit behavior
* {@linkplain #withPotentialRepeat with potential repeats} of the entire
* execution chain.
* <p>Furthermore, support for timeouts has been moved down the execution
* chain in order to include execution of {@link org.junit.Before @Before}
* and {@link org.junit.After @After} methods within the timed execution.
* Note that this differs from the default JUnit behavior of executing
* {@code @Before} and {@code @After} methods in the main thread while
* executing the actual test method in a separate thread. Thus, the net
* effect is that {@code @Before} and {@code @After} methods will be
* executed in the same thread as the test method. As a consequence,
* JUnit-specified timeouts will work fine in combination with Spring
* transactions. However, JUnit-specific timeouts still differ from
* Spring-specific timeouts in that the former execute in a separate
* thread while the latter simply execute in the main thread (like regular
* tests).
* @see #methodInvoker(FrameworkMethod, Object)
* @see #withBeforeTestExecutionCallbacks(FrameworkMethod, Object, Statement)
* @see #withAfterTestExecutionCallbacks(FrameworkMethod, Object, Statement)
* @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement)
* @see #withBefores(FrameworkMethod, Object, Statement)
* @see #withAfters(FrameworkMethod, Object, Statement)
* @see #withRulesReflectively(FrameworkMethod, Object, Statement)
* @see #withPotentialRepeat(FrameworkMethod, Object, Statement)
* @see #withPotentialTimeout(FrameworkMethod, Object, Statement)
*/
@Override
protected Statement methodBlock(FrameworkMethod frameworkMethod) {
Object testInstance;
try {
testInstance = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest();
}
}.run();
}
catch (Throwable ex) {
return new Fail(ex);
}
Statement statement = methodInvoker(frameworkMethod, testInstance);
statement = withBeforeTestExecutionCallbacks(frameworkMethod, testInstance, statement);
statement = withAfterTestExecutionCallbacks(frameworkMethod, testInstance, statement);
statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);
statement = withBefores(frameworkMethod, testInstance, statement);
statement = withAfters(frameworkMethod, testInstance, statement);
statement = withRulesReflectively(frameworkMethod, testInstance, statement);
statement = withPotentialRepeat(frameworkMethod, testInstance, statement);
statement = withPotentialTimeout(frameworkMethod, testInstance, statement);
return statement;
}
/**
* Invoke JUnit's private {@code withRules()} method using reflection.
*/
private Statement withRulesReflectively(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
return (Statement) ReflectionUtils.invokeMethod(withRulesMethod, this, frameworkMethod, testInstance, statement);
}
/**
* Return {@code true} if {@link Ignore @Ignore} is present for the supplied
* {@linkplain FrameworkMethod test method} or if the test method is disabled
* via {@code @IfProfileValue}.
* @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class)
*/
protected boolean isTestMethodIgnored(FrameworkMethod frameworkMethod) {
Method method = frameworkMethod.getMethod();
return (method.isAnnotationPresent(Ignore.class) ||
!ProfileValueUtils.isTestEnabledInThisEnvironment(method, getTestClass().getJavaClass()));
}
/**
* Perform the same logic as
* {@link BlockJUnit4ClassRunner#possiblyExpectingExceptions(FrameworkMethod, Object, Statement)}
* except that the <em>expected exception</em> is retrieved using
* {@link #getExpectedException(FrameworkMethod)}.
*/
@Override
protected Statement possiblyExpectingExceptions(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
Class<? extends Throwable> expectedException = getExpectedException(frameworkMethod);
return (expectedException != null ? new ExpectException(next, expectedException) : next);
}
/**
* Get the {@code exception} that the supplied {@linkplain FrameworkMethod
* test method} is expected to throw.
* <p>Supports JUnit's {@link Test#expected() @Test(expected=...)} annotation.
* <p>Can be overridden by subclasses.
* @return the expected exception, or {@code null} if none was specified
*/
protected Class<? extends Throwable> getExpectedException(FrameworkMethod frameworkMethod) {
Test test = frameworkMethod.getAnnotation(Test.class);
return (test != null && test.expected() != Test.None.class ? test.expected() : null);
}
/**
* Perform the same logic as
* {@link BlockJUnit4ClassRunner#withPotentialTimeout(FrameworkMethod, Object, Statement)}
* but with additional support for Spring's {@code @Timed} annotation.
* <p>Supports both Spring's {@link org.springframework.test.annotation.Timed @Timed}
* and JUnit's {@link Test#timeout() @Test(timeout=...)} annotations, but not both
* simultaneously.
* @return either a {@link SpringFailOnTimeout}, a {@link FailOnTimeout},
* or the supplied {@link Statement} as appropriate
* @see #getSpringTimeout(FrameworkMethod)
* @see #getJUnitTimeout(FrameworkMethod)
*/
@Override
@SuppressWarnings("deprecation")
protected Statement withPotentialTimeout(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
Statement statement = null;
long springTimeout = getSpringTimeout(frameworkMethod);
long junitTimeout = getJUnitTimeout(frameworkMethod);
if (springTimeout > 0 && junitTimeout > 0) {
String msg = String.format("Test method [%s] has been configured with Spring's @Timed(millis=%s) and " +
"JUnit's @Test(timeout=%s) annotations, but only one declaration of a 'timeout' is " +
"permitted per test method.", frameworkMethod.getMethod(), springTimeout, junitTimeout);
logger.error(msg);
throw new IllegalStateException(msg);
}
else if (springTimeout > 0) {
statement = new SpringFailOnTimeout(next, springTimeout);
}
else if (junitTimeout > 0) {
statement = FailOnTimeout.builder().withTimeout(junitTimeout, TimeUnit.MILLISECONDS).build(next);
}
else {
statement = next;
}
return statement;
}
/**
* Retrieve the configured JUnit {@code timeout} from the {@link Test @Test}
* annotation on the supplied {@linkplain FrameworkMethod test method}.
* @return the timeout, or {@code 0} if none was specified
*/
protected long getJUnitTimeout(FrameworkMethod frameworkMethod) {
Test test = frameworkMethod.getAnnotation(Test.class);
return (test != null && test.timeout() > 0 ? test.timeout() : 0);
}
/**
* Retrieve the configured Spring-specific {@code timeout} from the
* {@link org.springframework.test.annotation.Timed @Timed} annotation
* on the supplied {@linkplain FrameworkMethod test method}.
* @return the timeout, or {@code 0} if none was specified
* @see TestAnnotationUtils#getTimeout(Method)
*/
protected long getSpringTimeout(FrameworkMethod frameworkMethod) {
return TestAnnotationUtils.getTimeout(frameworkMethod.getMethod());
}
/**
* Wrap the supplied {@link Statement} with a {@code RunBeforeTestExecutionCallbacks}
* statement, thus preserving the default functionality while adding support for the
* Spring TestContext Framework.
* @see RunBeforeTestExecutionCallbacks
*/
protected Statement withBeforeTestExecutionCallbacks(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
return new RunBeforeTestExecutionCallbacks(statement, testInstance, frameworkMethod.getMethod(), getTestContextManager());
}
/**
* Wrap the supplied {@link Statement} with a {@code RunAfterTestExecutionCallbacks}
* statement, thus preserving the default functionality while adding support for the
* Spring TestContext Framework.
* @see RunAfterTestExecutionCallbacks
*/
protected Statement withAfterTestExecutionCallbacks(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
return new RunAfterTestExecutionCallbacks(statement, testInstance, frameworkMethod.getMethod(), getTestContextManager());
}
/**
* Wrap the {@link Statement} returned by the parent implementation with a
* {@code RunBeforeTestMethodCallbacks} statement, thus preserving the
* default functionality while adding support for the Spring TestContext
* Framework.
* @see RunBeforeTestMethodCallbacks
*/
@Override
protected Statement withBefores(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement);
return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(), getTestContextManager());
}
/**
* Wrap the {@link Statement} returned by the parent implementation with a
* {@code RunAfterTestMethodCallbacks} statement, thus preserving the
* default functionality while adding support for the Spring TestContext
* Framework.
* @see RunAfterTestMethodCallbacks
*/
@Override
protected Statement withAfters(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement);
return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(), getTestContextManager());
}
/**
* Wrap the supplied {@link Statement} with a {@code SpringRepeat} statement.
* <p>Supports Spring's {@link org.springframework.test.annotation.Repeat @Repeat}
* annotation.
* @see TestAnnotationUtils#getRepeatCount(Method)
* @see SpringRepeat
*/
protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
return new SpringRepeat(next, frameworkMethod.getMethod());
}
}