org.springframework.test.context.junit4.SpringJUnit4ClassRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.test.context.junit4.SpringJUnit4ClassRunner.java

Source

/*
 * Copyright 2002-2017 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.lang.Nullable;
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.");

        Method method = ReflectionUtils.findMethod(SpringJUnit4ClassRunner.class, "withRules",
                FrameworkMethod.class, Object.class, Statement.class);
        Assert.state(method != null, "SpringJUnit4ClassRunner requires JUnit 4.12 or higher");
        ReflectionUtils.makeAccessible(method);
        withRulesMethod = method;
    }

    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) {
        Object result = ReflectionUtils.invokeMethod(withRulesMethod, this, frameworkMethod, testInstance,
                statement);
        Assert.state(result instanceof Statement, "withRules mismatch");
        return (Statement) result;
    }

    /**
     * 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
     */
    @Nullable
    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());
    }

}