Introduce programmatic tx mgmt in the TCF

Historically, Spring's JUnit 3.8 TestCase class hierarchy supported
programmatic transaction management of "test-managed transactions" via
the protected endTransaction() and startNewTransaction() methods in
AbstractTransactionalSpringContextTests.

The Spring TestContext Framework (TCF) was introduced in Spring 2.5 to
supersede the legacy JUnit 3.8 support classes; however, prior to this
commit the TCF has not provided support for programmatically starting
or stopping the test-managed transaction.

This commit introduces a TestTransaction class in the TCF that provides
static utility methods for programmatically interacting with
test-managed transactions. Specifically, the following features are
supported by TestTransaction and its collaborators.

 - End the current test-managed transaction.

 - Start a new test-managed transaction, using the default rollback
   semantics configured via @TransactionConfiguration and @Rollback.

 - Flag the current test-managed transaction to be committed.

 - Flag the current test-managed transaction to be rolled back.

Implementation Details:

 - TransactionContext is now a top-level, package private class.

 - The existing test transaction management logic has been extracted
   from TransactionalTestExecutionListener into TransactionContext.

 - The current TransactionContext is stored in a
   NamedInheritableThreadLocal that is managed by
   TransactionContextHolder.

 - TestTransaction defines the end-user API, interacting with the
   TransactionContextHolder behind the scenes.

 - TransactionalTestExecutionListener now delegates to
   TransactionContext completely for starting and ending transactions.

Issue: SPR-5079
This commit is contained in:
Sam Brannen
2014-06-24 13:48:20 +02:00
parent 526b463474
commit f667e43ca2
8 changed files with 663 additions and 108 deletions

View File

@@ -40,7 +40,9 @@ public class EmptyDatabaseConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().build();
return new EmbeddedDatabaseBuilder()//
.setName("empty-sql-scripts-test-db")//
.build();
}
}

View File

@@ -42,6 +42,7 @@ public class PopulatedSchemaDatabaseConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()//
.setName("populated-sql-scripts-test-db")//
.addScript("classpath:/org/springframework/test/context/jdbc/schema.sql") //
.build();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 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.
@@ -19,6 +19,7 @@ package org.springframework.test.context.transaction;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.After;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.test.annotation.Rollback;
@@ -70,6 +71,7 @@ public class TransactionalTestExecutionListenerTests {
when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("transactionalTest"));
assertFalse(instance.invoked);
TransactionContextHolder.removeCurrentTransactionContext();
listener.beforeTestMethod(testContext);
assertEquals(invokedInTx, instance.invoked);
}
@@ -82,6 +84,7 @@ public class TransactionalTestExecutionListenerTests {
when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("nonTransactionalTest"));
assertFalse(instance.invoked);
TransactionContextHolder.removeCurrentTransactionContext();
listener.beforeTestMethod(testContext);
assertFalse(instance.invoked);
}
@@ -100,6 +103,7 @@ public class TransactionalTestExecutionListenerTests {
when(tm.getTransaction(Mockito.any(TransactionDefinition.class))).thenReturn(new SimpleTransactionStatus());
assertFalse(instance.invoked);
TransactionContextHolder.removeCurrentTransactionContext();
listener.beforeTestMethod(testContext);
listener.afterTestMethod(testContext);
assertTrue(instance.invoked);
@@ -112,6 +116,7 @@ public class TransactionalTestExecutionListenerTests {
when(testContext.getTestMethod()).thenReturn(clazz.getDeclaredMethod("nonTransactionalTest"));
assertFalse(instance.invoked);
TransactionContextHolder.removeCurrentTransactionContext();
listener.beforeTestMethod(testContext);
listener.afterTestMethod(testContext);
assertFalse(instance.invoked);
@@ -133,6 +138,11 @@ public class TransactionalTestExecutionListenerTests {
assertEquals(rollback, listener.isRollback(testContext));
}
@After
public void cleanUpThreadLocalStateForSubsequentTestClassesInSuite() {
TransactionContextHolder.removeCurrentTransactionContext();
}
@Test
public void beforeTestMethodWithTransactionalDeclaredOnClassLocally() throws Exception {
assertBeforeTestMethodWithTransactionalTestMethod(TransactionalDeclaredOnClassLocallyTestCase.class);
@@ -192,14 +202,12 @@ public class TransactionalTestExecutionListenerTests {
@Test
public void retrieveConfigurationAttributesWithMissingTransactionConfiguration() throws Exception {
assertTransactionConfigurationAttributes(MissingTransactionConfigurationTestCase.class, "",
true);
assertTransactionConfigurationAttributes(MissingTransactionConfigurationTestCase.class, "", true);
}
@Test
public void retrieveConfigurationAttributesWithEmptyTransactionConfiguration() throws Exception {
assertTransactionConfigurationAttributes(EmptyTransactionConfigurationTestCase.class, "",
true);
assertTransactionConfigurationAttributes(EmptyTransactionConfigurationTestCase.class, "", true);
}
@Test

View File

@@ -0,0 +1,284 @@
/*
* Copyright 2002-2014 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.transaction.programmatic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.test.context.transaction.TestTransaction;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.Assert.*;
import static org.springframework.test.transaction.TransactionTestUtils.*;
/**
* Integration tests that verify support for programmatic transaction management
* within the <em>Spring TestContext Framework</em>.
*
* @author Sam Brannen
* @since 4.1
*/
@ContextConfiguration
public class ProgrammaticTxMgmtTests extends AbstractTransactionalJUnit4SpringContextTests {
@Rule
public TestName testName = new TestName();
@BeforeTransaction
public void beforeTransaction() {
deleteFromTables("user");
executeSqlScript("classpath:/org/springframework/test/context/jdbc/data.sql", false);
}
@AfterTransaction
public void afterTransaction() {
String method = testName.getMethodName();
switch (method) {
case "commitTxAndStartNewTx": {
assertUsers("Dogbert");
break;
}
case "commitTxButDoNotStartNewTx": {
assertUsers("Dogbert");
break;
}
case "rollbackTxAndStartNewTx": {
assertUsers("Dilbert");
break;
}
case "rollbackTxButDoNotStartNewTx": {
assertUsers("Dilbert");
break;
}
case "rollbackTxAndStartNewTxWithDefaultCommitSemantics": {
assertUsers("Dilbert", "Dogbert");
break;
}
case "startTxWithExistingTransaction": {
assertUsers("Dilbert");
break;
}
default: {
fail("missing 'after transaction' assertion for test method: " + method);
}
}
}
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void isActiveWithNonExistentTransactionContext() {
assertFalse(TestTransaction.isActive());
}
@Test(expected = IllegalStateException.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void flagForRollbackWithNonExistentTransactionContext() {
TestTransaction.flagForRollback();
}
@Test(expected = IllegalStateException.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void flagForCommitWithNonExistentTransactionContext() {
TestTransaction.flagForCommit();
}
@Test(expected = IllegalStateException.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void isFlaggedForRollbackWithNonExistentTransactionContext() {
TestTransaction.isFlaggedForRollback();
}
@Test(expected = IllegalStateException.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void startTxWithNonExistentTransactionContext() {
TestTransaction.start();
}
@Test(expected = IllegalStateException.class)
public void startTxWithExistingTransaction() {
TestTransaction.start();
}
@Test(expected = IllegalStateException.class)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void endTxWithNonExistentTransactionContext() {
TestTransaction.end();
}
@Test
public void commitTxAndStartNewTx() {
assertInTransaction(true);
assertTrue(TestTransaction.isActive());
assertUsers("Dilbert");
deleteFromTables("user");
assertUsers();
// Commit
TestTransaction.flagForCommit();
assertFalse(TestTransaction.isFlaggedForRollback());
TestTransaction.end();
assertInTransaction(false);
assertFalse(TestTransaction.isActive());
assertUsers();
executeSqlScript("classpath:/org/springframework/test/context/jdbc/data-add-dogbert.sql", false);
assertUsers("Dogbert");
TestTransaction.start();
assertInTransaction(true);
assertTrue(TestTransaction.isActive());
}
@Test
public void commitTxButDoNotStartNewTx() {
assertInTransaction(true);
assertTrue(TestTransaction.isActive());
assertUsers("Dilbert");
deleteFromTables("user");
assertUsers();
// Commit
TestTransaction.flagForCommit();
assertFalse(TestTransaction.isFlaggedForRollback());
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertInTransaction(false);
assertUsers();
executeSqlScript("classpath:/org/springframework/test/context/jdbc/data-add-dogbert.sql", false);
assertUsers("Dogbert");
}
@Test
public void rollbackTxAndStartNewTx() {
assertInTransaction(true);
assertTrue(TestTransaction.isActive());
assertUsers("Dilbert");
deleteFromTables("user");
assertUsers();
// Rollback (automatically)
assertTrue(TestTransaction.isFlaggedForRollback());
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertInTransaction(false);
assertUsers("Dilbert");
// Start new transaction with default rollback semantics
TestTransaction.start();
assertInTransaction(true);
assertTrue(TestTransaction.isFlaggedForRollback());
assertTrue(TestTransaction.isActive());
executeSqlScript("classpath:/org/springframework/test/context/jdbc/data-add-dogbert.sql", false);
assertUsers("Dilbert", "Dogbert");
}
@Test
public void rollbackTxButDoNotStartNewTx() {
assertInTransaction(true);
assertTrue(TestTransaction.isActive());
assertUsers("Dilbert");
deleteFromTables("user");
assertUsers();
// Rollback (automatically)
assertTrue(TestTransaction.isFlaggedForRollback());
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertInTransaction(false);
assertUsers("Dilbert");
}
@Test
@Rollback(false)
public void rollbackTxAndStartNewTxWithDefaultCommitSemantics() {
assertInTransaction(true);
assertTrue(TestTransaction.isActive());
assertUsers("Dilbert");
deleteFromTables("user");
assertUsers();
// Rollback
TestTransaction.flagForRollback();
assertTrue(TestTransaction.isFlaggedForRollback());
TestTransaction.end();
assertFalse(TestTransaction.isActive());
assertInTransaction(false);
assertUsers("Dilbert");
// Start new transaction with default commit semantics
TestTransaction.start();
assertInTransaction(true);
assertFalse(TestTransaction.isFlaggedForRollback());
assertTrue(TestTransaction.isActive());
executeSqlScript("classpath:/org/springframework/test/context/jdbc/data-add-dogbert.sql", false);
assertUsers("Dilbert", "Dogbert");
}
// -------------------------------------------------------------------------
private void assertUsers(String... users) {
List<Map<String, Object>> results = jdbcTemplate.queryForList("select name from user");
List<String> names = new ArrayList<String>();
for (Map<String, Object> map : results) {
names.add((String) map.get("name"));
}
assertEquals(Arrays.asList(users), names);
}
// -------------------------------------------------------------------------
@Configuration
static class Config {
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()//
.setName("programmatic-tx-mgmt-test-db")//
.addScript("classpath:/org/springframework/test/context/jdbc/schema.sql") //
.build();
}
}
}