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:
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toCollection;
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests that verify proper concurrency support between a
|
||||
* {@link TestContextManager} and the {@link TestContext} it manages
|
||||
* when a registered {@link TestExecutionListener} updates the mutable
|
||||
* state and attributes of the context from concurrently executing threads.
|
||||
*
|
||||
* <p>In other words, these tests verify that mutated state and attributes
|
||||
* are only be visible to the thread in which the mutation occurred.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 5.0
|
||||
*/
|
||||
public class TestContextConcurrencyTests {
|
||||
|
||||
private static Set<String> expectedMethods = stream(TestCase.class.getDeclaredMethods()).map(
|
||||
Method::getName).collect(toCollection(TreeSet::new));
|
||||
|
||||
private static final Set<String> actualMethods = Collections.synchronizedSet(new TreeSet<>());
|
||||
|
||||
private static final TestCase testInstance = new TestCase();
|
||||
|
||||
|
||||
@Test
|
||||
public void invokeTestContextManagerFromConcurrentThreads() {
|
||||
TestContextManager tcm = new TestContextManager(TestCase.class);
|
||||
|
||||
// Run the actual test several times in order to increase the chance of threads
|
||||
// stepping on each others' toes by overwriting the same mutable state in the
|
||||
// TestContext.
|
||||
IntStream.range(1, 20).forEach(i -> {
|
||||
actualMethods.clear();
|
||||
// Execute TestExecutionListener in parallel, thereby simulating parallel
|
||||
// test method execution.
|
||||
stream(TestCase.class.getDeclaredMethods()).parallel().forEach(testMethod -> {
|
||||
try {
|
||||
tcm.beforeTestClass();
|
||||
tcm.beforeTestMethod(testInstance, testMethod);
|
||||
// no need to invoke the actual test method
|
||||
tcm.afterTestMethod(testInstance, testMethod, null);
|
||||
tcm.afterTestClass();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
});
|
||||
assertThat(actualMethods, equalTo(expectedMethods));
|
||||
});
|
||||
assertEquals(0, tcm.getTestContext().attributeNames().length);
|
||||
}
|
||||
|
||||
|
||||
@TestExecutionListeners(TrackingListener.class)
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestCase {
|
||||
|
||||
void test_001() {
|
||||
}
|
||||
|
||||
void test_002() {
|
||||
}
|
||||
|
||||
void test_003() {
|
||||
}
|
||||
|
||||
void test_004() {
|
||||
}
|
||||
|
||||
void test_005() {
|
||||
}
|
||||
|
||||
void test_006() {
|
||||
}
|
||||
|
||||
void test_007() {
|
||||
}
|
||||
|
||||
void test_008() {
|
||||
}
|
||||
|
||||
void test_009() {
|
||||
}
|
||||
|
||||
void test_010() {
|
||||
}
|
||||
}
|
||||
|
||||
private static class TrackingListener implements TestExecutionListener {
|
||||
|
||||
private ThreadLocal<String> methodName = new ThreadLocal<>();
|
||||
|
||||
|
||||
@Override
|
||||
public void beforeTestMethod(TestContext testContext) throws Exception {
|
||||
String name = testContext.getTestMethod().getName();
|
||||
actualMethods.add(name);
|
||||
testContext.setAttribute("method", name);
|
||||
this.methodName.set(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTestMethod(TestContext testContext) throws Exception {
|
||||
assertEquals(this.methodName.get(), testContext.getAttribute("method"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user