/*
* 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;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* {@code TestContextManager} is the main entry point into the <em>Spring
* TestContext Framework</em>.
*
* <p>Specifically, a {@code TestContextManager} is responsible for managing a
* single {@link TestContext} and signaling events to all registered
* {@link TestExecutionListener TestExecutionListeners} at the following test
* execution points:
*
* <ul>
* <li>{@link #beforeTestClass() before test class execution}: prior to any
* <em>before class callbacks</em> of a particular testing framework (e.g.,
* JUnit 4's {@link org.junit.BeforeClass @BeforeClass})</li>
* <li>{@link #prepareTestInstance test instance preparation}:
* immediately following instantiation of the test class</li>
* <li>{@link #beforeTestMethod before test setup}:
* prior to any <em>before method callbacks</em> of a particular testing framework
* (e.g., JUnit 4's {@link org.junit.Before @Before})</li>
* <li>{@link #beforeTestExecution before test execution}:
* immediately before execution of the {@linkplain java.lang.reflect.Method
* test method} but after test setup</li>
* <li>{@link #afterTestExecution after test execution}:
* immediately after execution of the {@linkplain java.lang.reflect.Method
* test method} but before test tear down</li>
* <li>{@link #afterTestMethod(Object, Method, Throwable) after test tear down}:
* after any <em>after method callbacks</em> of a particular testing
* framework (e.g., JUnit 4's {@link org.junit.After @After})</li>
* <li>{@link #afterTestClass() after test class execution}: after any
* <em>after class callbacks</em> of a particular testing framework (e.g., JUnit 4's
* {@link org.junit.AfterClass @AfterClass})</li>
* </ul>
*
* <p>Support for loading and accessing
* {@linkplain org.springframework.context.ApplicationContext application contexts},
* dependency injection of test instances,
* {@linkplain org.springframework.transaction.annotation.Transactional transactional}
* execution of test methods, etc. is provided by
* {@link SmartContextLoader ContextLoaders} and {@link TestExecutionListener
* TestExecutionListeners}, which are configured via
* {@link ContextConfiguration @ContextConfiguration} and
* {@link TestExecutionListeners @TestExecutionListeners}.
*
* <p>Bootstrapping of the {@code TestContext}, the default {@code ContextLoader},
* default {@code TestExecutionListeners}, and their collaborators is performed
* by a {@link TestContextBootstrapper}, which is configured via
* {@link BootstrapWith @BootstrapWith}.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 2.5
* @see BootstrapWith
* @see BootstrapContext
* @see TestContextBootstrapper
* @see TestContext
* @see TestExecutionListener
* @see TestExecutionListeners
* @see ContextConfiguration
* @see ContextHierarchy
*/
public class TestContextManager {
private static final Log logger = LogFactory.getLog(TestContextManager.class);
private final TestContext testContext;
private final ThreadLocal<TestContext> testContextHolder = new ThreadLocal<TestContext>() {
protected TestContext initialValue() {
return copyTestContext(TestContextManager.this.testContext);
}
};
private final List<TestExecutionListener> testExecutionListeners = new ArrayList<>();
/**
* Construct a new {@code TestContextManager} for the supplied {@linkplain Class test class}.
* <p>Delegates to {@link #TestContextManager(TestContextBootstrapper)} with
* the {@link TestContextBootstrapper} configured for the test class. If the
* {@link BootstrapWith @BootstrapWith} annotation is present on the test
* class, either directly or as a meta-annotation, then its
* {@link BootstrapWith#value value} will be used as the bootstrapper type;
* otherwise, the {@link org.springframework.test.context.support.DefaultTestContextBootstrapper
* DefaultTestContextBootstrapper} will be used.
* @param testClass the test class to be managed
* @see #TestContextManager(TestContextBootstrapper)
*/
public TestContextManager(Class<?> testClass) {
this(BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass)));
}
/**
* Construct a new {@code TestContextManager} using the supplied {@link TestContextBootstrapper}
* and {@linkplain #registerTestExecutionListeners register} the necessary
* {@link TestExecutionListener TestExecutionListeners}.
* <p>Delegates to the supplied {@code TestContextBootstrapper} for building
* the {@code TestContext} and retrieving the {@code TestExecutionListeners}.
* @param testContextBootstrapper the bootstrapper to use
* @see TestContextBootstrapper#buildTestContext
* @see TestContextBootstrapper#getTestExecutionListeners
* @see #registerTestExecutionListeners
*/
public TestContextManager(TestContextBootstrapper testContextBootstrapper) {
this.testContext = testContextBootstrapper.buildTestContext();
registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners());
}
/**
* Get the {@link TestContext} managed by this {@code TestContextManager}.
*/
public final TestContext getTestContext() {
return this.testContextHolder.get();
}
/**
* Register the supplied list of {@link TestExecutionListener TestExecutionListeners}
* by appending them to the list of listeners used by this {@code TestContextManager}.
* @see #registerTestExecutionListeners(TestExecutionListener...)
*/
public void registerTestExecutionListeners(List<TestExecutionListener> testExecutionListeners) {
registerTestExecutionListeners(testExecutionListeners.toArray(new TestExecutionListener[testExecutionListeners.size()]));
}
/**
* Register the supplied array of {@link TestExecutionListener TestExecutionListeners}
* by appending them to the list of listeners used by this {@code TestContextManager}.
*/
public void registerTestExecutionListeners(TestExecutionListener... testExecutionListeners) {
for (TestExecutionListener listener : testExecutionListeners) {
if (logger.isTraceEnabled()) {
logger.trace("Registering TestExecutionListener: " + listener);
}
this.testExecutionListeners.add(listener);
}
}
/**
* Get the current {@link TestExecutionListener TestExecutionListeners}
* registered for this {@code TestContextManager}.
* <p>Allows for modifications, e.g. adding a listener to the beginning of the list.
* However, make sure to keep the list stable while actually executing tests.
*/
public final List<TestExecutionListener> getTestExecutionListeners() {
return this.testExecutionListeners;
}
/**
* Get a copy of the {@link TestExecutionListener TestExecutionListeners}
* registered for this {@code TestContextManager} in reverse order.
*/
private List<TestExecutionListener> getReversedTestExecutionListeners() {
List<TestExecutionListener> listenersReversed = new ArrayList<>(getTestExecutionListeners());
Collections.reverse(listenersReversed);
return listenersReversed;
}
/**
* Hook for pre-processing a test class <em>before</em> execution of any
* tests within the class. Should be called prior to any framework-specific
* <em>before class methods</em> (e.g., methods annotated with JUnit 4's
* {@link org.junit.BeforeClass @BeforeClass}).
* <p>An attempt will be made to give each registered
* {@link TestExecutionListener} a chance to pre-process the test class
* execution. If a listener throws an exception, however, the remaining
* registered listeners will <strong>not</strong> be called.
* @throws Exception if a registered TestExecutionListener throws an
* exception
* @since 3.0
* @see #getTestExecutionListeners()
*/
public void beforeTestClass() throws Exception {
Class<?> testClass = getTestContext().getTestClass();
if (logger.isTraceEnabled()) {
logger.trace("beforeTestClass(): class [" + testClass.getName() + "]");
}
getTestContext().updateState(null, null, null);
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
try {
testExecutionListener.beforeTestClass(getTestContext());
}
catch (Throwable ex) {
logException(ex, "beforeTestClass", testExecutionListener, testClass);
ReflectionUtils.rethrowException(ex);
}
}
}
/**
* Hook for preparing a test instance prior to execution of any individual
* test methods, for example for injecting dependencies, etc. Should be
* called immediately after instantiation of the test instance.
* <p>The managed {@link TestContext} will be updated with the supplied
* {@code testInstance}.
* <p>An attempt will be made to give each registered
* {@link TestExecutionListener} a chance to prepare the test instance. If a
* listener throws an exception, however, the remaining registered listeners
* will <strong>not</strong> be called.
* @param testInstance the test instance to prepare (never {@code null})
* @throws Exception if a registered TestExecutionListener throws an exception
* @see #getTestExecutionListeners()
*/
public void prepareTestInstance(Object testInstance) throws Exception {
Assert.notNull(testInstance, "Test instance must not be null");
if (logger.isTraceEnabled()) {
logger.trace("prepareTestInstance(): instance [" + testInstance + "]");
}
getTestContext().updateState(testInstance, null, null);
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
try {
testExecutionListener.prepareTestInstance(getTestContext());
}
catch (Throwable ex) {
if (logger.isErrorEnabled()) {
logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
"] to prepare test instance [" + testInstance + "]", ex);
}
ReflectionUtils.rethrowException(ex);
}
}
}
/**
* Hook for pre-processing a test <em>before</em> execution of <em>before</em>
* lifecycle callbacks of the underlying test framework — for example,
* setting up test fixtures, starting a transaction, etc.
* <p>This method <strong>must</strong> be called immediately prior to
* framework-specific <em>before</em> lifecycle callbacks (e.g., methods
* annotated with JUnit 4's {@link org.junit.Before @Before}). For historical
* reasons, this method is named {@code beforeTestMethod}. Since the
* introduction of {@link #beforeTestExecution}, a more suitable name for
* this method might be something like {@code beforeTestSetUp} or
* {@code beforeEach}; however, it is unfortunately impossible to rename
* this method due to backward compatibility concerns.
* <p>The managed {@link TestContext} will be updated with the supplied
* {@code testInstance} and {@code testMethod}.
* <p>An attempt will be made to give each registered
* {@link TestExecutionListener} a chance to perform its pre-processing.
* If a listener throws an exception, however, the remaining registered
* listeners will <strong>not</strong> be called.
* @param testInstance the current test instance (never {@code null})
* @param testMethod the test method which is about to be executed on the
* test instance
* @throws Exception if a registered TestExecutionListener throws an exception
* @see #afterTestMethod
* @see #beforeTestExecution
* @see #afterTestExecution
* @see #getTestExecutionListeners()
*/
public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception {
String callbackName = "beforeTestMethod";
prepareForBeforeCallback(callbackName, testInstance, testMethod);
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
try {
testExecutionListener.beforeTestMethod(getTestContext());
}
catch (Throwable ex) {
handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod);
}
}
}
/**
* Hook for pre-processing a test <em>immediately before</em> execution of
* the {@linkplain java.lang.reflect.Method test method} in the supplied
* {@linkplain TestContext test context} — for example, for timing
* or logging purposes.
* <p>This method <strong>must</strong> be called after framework-specific
* <em>before</em> lifecycle callbacks (e.g., methods annotated with JUnit 4's
* {@link org.junit.Before @Before}).
* <p>The managed {@link TestContext} will be updated with the supplied
* {@code testInstance} and {@code testMethod}.
* <p>An attempt will be made to give each registered
* {@link TestExecutionListener} a chance to perform its pre-processing.
* If a listener throws an exception, however, the remaining registered
* listeners will <strong>not</strong> be called.
* @param testInstance the current test instance (never {@code null})
* @param testMethod the test method which is about to be executed on the
* test instance
* @throws Exception if a registered TestExecutionListener throws an exception
* @since 5.0
* @see #beforeTestMethod
* @see #afterTestMethod
* @see #beforeTestExecution
* @see #afterTestExecution
* @see #getTestExecutionListeners()
*/
public void beforeTestExecution(Object testInstance, Method testMethod) throws Exception {
String callbackName = "beforeTestExecution";
prepareForBeforeCallback(callbackName, testInstance, testMethod);
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
try {
testExecutionListener.beforeTestExecution(getTestContext());
}
catch (Throwable ex) {
handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod);
}
}
}
/**
* Hook for post-processing a test <em>immediately after</em> execution of
* the {@linkplain java.lang.reflect.Method test method} in the supplied
* {@linkplain TestContext test context} — for example, for timing
* or logging purposes.
* <p>This method <strong>must</strong> be called before framework-specific
* <em>after</em> lifecycle callbacks (e.g., methods annotated with JUnit 4's
* {@link org.junit.After @After}).
* <p>The managed {@link TestContext} will be updated with the supplied
* {@code testInstance}, {@code testMethod}, and {@code exception}.
* <p>Each registered {@link TestExecutionListener} will be given a chance
* to perform its post-processing. If a listener throws an exception, the
* remaining registered listeners will still be called. After all listeners
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* order in which they were registered.
* @param testInstance the current test instance (never {@code null})
* @param testMethod the test method which has just been executed on the
* test instance
* @param exception the exception that was thrown during execution of the
* test method or by a TestExecutionListener, or {@code null} if none
* was thrown
* @throws Exception if a registered TestExecutionListener throws an exception
* @since 5.0
* @see #beforeTestMethod
* @see #afterTestMethod
* @see #beforeTestExecution
* @see #getTestExecutionListeners()
* @see Throwable#addSuppressed(Throwable)
*/
public void afterTestExecution(Object testInstance, Method testMethod, Throwable exception) throws Exception {
String callbackName = "afterTestExecution";
prepareForAfterCallback(callbackName, testInstance, testMethod, exception);
Throwable afterTestExecutionException = null;
// Traverse the TestExecutionListeners in reverse order to ensure proper
// "wrapper"-style execution of listeners.
for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) {
try {
testExecutionListener.afterTestExecution(getTestContext());
}
catch (Throwable ex) {
logException(ex, callbackName, testExecutionListener, testInstance, testMethod);
if (afterTestExecutionException == null) {
afterTestExecutionException = ex;
}
else {
afterTestExecutionException.addSuppressed(ex);
}
}
}
if (afterTestExecutionException != null) {
ReflectionUtils.rethrowException(afterTestExecutionException);
}
}
/**
* Hook for post-processing a test <em>after</em> execution of <em>after</em>
* lifecycle callbacks of the underlying test framework — for example,
* tearing down test fixtures, ending a transaction, etc.
* <p>This method <strong>must</strong> be called immediately after
* framework-specific <em>after</em> lifecycle callbacks (e.g., methods
* annotated with JUnit 4's {@link org.junit.After @After}). For historical
* reasons, this method is named {@code afterTestMethod}. Since the
* introduction of {@link #afterTestExecution}, a more suitable name for
* this method might be something like {@code afterTestTearDown} or
* {@code afterEach}; however, it is unfortunately impossible to rename
* this method due to backward compatibility concerns.
* <p>The managed {@link TestContext} will be updated with the supplied
* {@code testInstance}, {@code testMethod}, and {@code exception}.
* <p>Each registered {@link TestExecutionListener} will be given a chance
* to perform its post-processing. If a listener throws an exception, the
* remaining registered listeners will still be called. After all listeners
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* @param testInstance the current test instance (never {@code null})
* @param testMethod the test method which has just been executed on the
* test instance
* @param exception the exception that was thrown during execution of the
* test method or by a TestExecutionListener, or {@code null} if none
* was thrown
* @throws Exception if a registered TestExecutionListener throws an exception
* @see #beforeTestMethod
* @see #beforeTestExecution
* @see #afterTestExecution
* @see #getTestExecutionListeners()
* @see Throwable#addSuppressed(Throwable)
*/
public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception {
String callbackName = "afterTestMethod";
prepareForAfterCallback(callbackName, testInstance, testMethod, exception);
Throwable afterTestMethodException = null;
// Traverse the TestExecutionListeners in reverse order to ensure proper
// "wrapper"-style execution of listeners.
for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) {
try {
testExecutionListener.afterTestMethod(getTestContext());
}
catch (Throwable ex) {
logException(ex, callbackName, testExecutionListener, testInstance, testMethod);
if (afterTestMethodException == null) {
afterTestMethodException = ex;
}
else {
afterTestMethodException.addSuppressed(ex);
}
}
}
if (afterTestMethodException != null) {
ReflectionUtils.rethrowException(afterTestMethodException);
}
}
/**
* Hook for post-processing a test class <em>after</em> execution of all
* tests within the class. Should be called after any framework-specific
* <em>after class methods</em> (e.g., methods annotated with JUnit 4's
* {@link org.junit.AfterClass @AfterClass}).
* <p>Each registered {@link TestExecutionListener} will be given a chance
* to perform its post-processing. If a listener throws an exception, the
* remaining registered listeners will still be called. After all listeners
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* @throws Exception if a registered TestExecutionListener throws an exception
* @since 3.0
* @see #getTestExecutionListeners()
* @see Throwable#addSuppressed(Throwable)
*/
public void afterTestClass() throws Exception {
Class<?> testClass = getTestContext().getTestClass();
if (logger.isTraceEnabled()) {
logger.trace("afterTestClass(): class [" + testClass.getName() + "]");
}
getTestContext().updateState(null, null, null);
Throwable afterTestClassException = null;
// Traverse the TestExecutionListeners in reverse order to ensure proper
// "wrapper"-style execution of listeners.
for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) {
try {
testExecutionListener.afterTestClass(getTestContext());
}
catch (Throwable ex) {
logException(ex, "afterTestClass", testExecutionListener, testClass);
if (afterTestClassException == null) {
afterTestClassException = ex;
}
else {
afterTestClassException.addSuppressed(ex);
}
}
}
this.testContextHolder.remove();
if (afterTestClassException != null) {
ReflectionUtils.rethrowException(afterTestClassException);
}
}
private void prepareForBeforeCallback(String callbackName, Object testInstance, Method testMethod) {
Assert.notNull(testInstance, "Test instance must not be null");
if (logger.isTraceEnabled()) {
logger.trace(String.format("%s(): instance [%s], method [%s]", callbackName, testInstance, testMethod));
}
getTestContext().updateState(testInstance, testMethod, null);
}
private void prepareForAfterCallback(String callbackName, Object testInstance, Method testMethod,
Throwable exception) {
Assert.notNull(testInstance, "Test instance must not be null");
if (logger.isTraceEnabled()) {
logger.trace(String.format("%s(): instance [%s], method [%s], exception [%s]", callbackName, testInstance,
testMethod, exception));
}
getTestContext().updateState(testInstance, testMethod, exception);
}
private void handleBeforeException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener,
Object testInstance, Method testMethod) throws Exception {
logException(ex, callbackName, testExecutionListener, testInstance, testMethod);
ReflectionUtils.rethrowException(ex);
}
private void logException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener,
Class<?> testClass) {
if (logger.isWarnEnabled()) {
logger.warn(String.format("Caught exception while invoking '%s' callback on " +
"TestExecutionListener [%s] for test class [%s]", callbackName, testExecutionListener,
testClass), ex);
}
}
private void logException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener,
Object testInstance, Method testMethod) {
if (logger.isWarnEnabled()) {
logger.warn(String.format("Caught exception while invoking '%s' callback on " +
"TestExecutionListener [%s] for test method [%s] and test instance [%s]",
callbackName, testExecutionListener, testMethod, testInstance), ex);
}
}
/**
* Attempt to create a copy of the supplied {@code TestContext} using its
* <em>copy constructor</em>.
*/
private static TestContext copyTestContext(TestContext testContext) {
Constructor<? extends TestContext> constructor = ClassUtils.getConstructorIfAvailable(testContext.getClass(),
testContext.getClass());
if (constructor != null) {
try {
ReflectionUtils.makeAccessible(constructor);
return constructor.newInstance(testContext);
}
catch (Exception ex) {
if (logger.isInfoEnabled()) {
logger.info(String.format("Failed to invoke copy constructor for [%s]; " +
"concurrent test execution is therefore likely not supported.",
testContext), ex);
}
}
}
// fallback to original instance
return testContext;
}
}