Support concurrent execution in TestContextManager & DefaultTestContext

Prior to this commit, executing tests concurrently in the TestContext
Framework (TCF) was unsupported and typically lead to unpredictable
results.

This commit addresses this core issue by supporting concurrent
execution in the TestContextManager and the DefaultTestContext.

Specifically, the TestContextManager now uses ThreadLocal storage for
the current TestContext, thereby ensuring that any registered
TestExecutionListeners and the TestContextManager itself operate on a
TestContext specific to the current thread.

In order to avoid repeatedly incurring the costs of the overhead of the
TCF bootstrapping process, the original TestContext built by the
TestContextBootstrapper is used as a template which is then passed to
the copy constructor of the concrete implementation of the TestContext
to create the context for the current thread. DefaultTestContext now
implements such a copy constructor, and all concrete implementations of
TestContext are encouraged to do the same.

If the TestContext built by the TestContextBootstrapper does not
provide a copy constructor, thread-safety and support for concurrency
are left completely to the implementation of the concrete TestContext.

Note, however, that this commit does not address any thread-safety or
concurrency issues in the ContextLoader SPI or its implementations.

Issue: SPR-5863
This commit is contained in:
Sam Brannen
2016-09-02 20:28:02 +02:00
parent ec7aefa858
commit a10a8e56df
4 changed files with 201 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -27,6 +27,14 @@ import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
* {@code TestContext} encapsulates the context in which a test is executed,
* agnostic of the actual testing framework in use.
*
* <p>As of Spring Framework 5.0, concrete implementations are highly encouraged
* to implement a <em>copy constructor</em> in order to allow the immutable state
* and attributes of a {@code TestContext} to be used as a template for additional
* contexts created for parallel test execution. The copy constructor must accept a
* single argument of the type of the concrete implementation. Any implementation
* that does not provide a copy constructor will likely fail in an environment
* that executes tests concurrently.
*
* @author Sam Brannen
* @since 2.5
*/

View File

@@ -16,6 +16,7 @@
package org.springframework.test.context;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
@@ -25,6 +26,7 @@ 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;
/**
@@ -92,6 +94,13 @@ public class TestContextManager {
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<>();
@@ -131,7 +140,7 @@ public class TestContextManager {
* Get the {@link TestContext} managed by this {@code TestContextManager}.
*/
public final TestContext getTestContext() {
return this.testContext;
return this.testContextHolder.get();
}
/**
@@ -482,6 +491,9 @@ public class TestContextManager {
}
}
}
this.testContextHolder.remove();
if (afterTestClassException != null) {
ReflectionUtils.rethrowException(afterTestClassException);
}
@@ -529,4 +541,31 @@ public class TestContextManager {
}
}
/**
* 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;
}
}

View File

@@ -56,6 +56,19 @@ public class DefaultTestContext implements TestContext {
private volatile Throwable testException;
/**
* <em>Copy constructor</em> for creating a new {@code DefaultTestContext}
* based on the immutable state and <em>attributes</em> of the supplied context.
*
* <p><em>Immutable state</em> includes all arguments supplied to
* {@link #DefaultTestContext(Class, MergedContextConfiguration, CacheAwareContextLoaderDelegate)}.
*/
public DefaultTestContext(DefaultTestContext testContext) {
this(testContext.testClass, testContext.mergedContextConfiguration,
testContext.cacheAwareContextLoaderDelegate);
testContext.attributes.forEach(this.attributes::put);
}
/**
* Construct a new {@code DefaultTestContext} from the supplied arguments.
* @param testClass the test class for this test context; never {@code null}