diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TestTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TestTransaction.java new file mode 100644 index 0000000000..daf82410ca --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TestTransaction.java @@ -0,0 +1,146 @@ +/* + * 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; + +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.transaction.TransactionStatus; + +/** + * {@code TestTransaction} provides a collection of static utility methods for + * programmatic interaction with test-managed transactions. + * + *
Test-managed transactions are transactions that are managed by the Spring TestContext Framework. + * + *
Support for {@code TestTransaction} is automatically available whenever + * the {@link TransactionalTestExecutionListener} is enabled. Note that the + * {@code TransactionalTestExecutionListener} is typically enabled by default, + * but it can also be manually enabled via the + * {@link TestExecutionListeners @TestExecutionListeners} annotation. + * + * @author Sam Brannen + * @since 4.1 + * @see TransactionalTestExecutionListener + */ +public class TestTransaction { + + /** + * Determine whether a test-managed transaction is currently active. + * @return {@code true} if a test-managed transaction is currently active + * @see #start() + * @see #end() + */ + public static boolean isActive() { + TransactionContext transactionContext = TransactionContextHolder.getCurrentTransactionContext(); + if (transactionContext != null) { + TransactionStatus transactionStatus = transactionContext.getTransactionStatus(); + return (transactionStatus != null) && (!transactionStatus.isCompleted()); + } + + // else + return false; + } + + /** + * Determine whether the current test-managed transaction has been + * {@linkplain #flagForRollback() flagged for rollback} or + * {@linkplain #flagForCommit() flagged for commit}. + * @return {@code true} if the current test-managed transaction is flagged + * to be rolled back; {@code false} if the current test-managed transaction + * is flagged to be committed + * @throws IllegalStateException if a transaction is not active for the + * current test + * @see #isActive() + * @see #flagForRollback() + * @see #flagForCommit() + */ + public static boolean isFlaggedForRollback() { + return requireCurrentTransactionContext().isFlaggedForRollback(); + } + + /** + * Flag the current test-managed transaction for rollback. + *
Invoking this method will not end the current transaction. + * Rather, the value of this flag will be used to determine whether or not + * the current test-managed transaction should be rolled back or committed + * once it is {@linkplain #end ended}. + * @throws IllegalStateException if a transaction is not active for the + * current test + * @see #isActive() + * @see #isFlaggedForRollback() + * @see #start() + * @see #end() + */ + public static void flagForRollback() { + setFlaggedForRollback(true); + } + + /** + * Flag the current test-managed transaction for commit. + *
Invoking this method will not end the current transaction. + * Rather, the value of this flag will be used to determine whether or not + * the current test-managed transaction should be rolled back or committed + * once it is {@linkplain #end ended}. + * @throws IllegalStateException if a transaction is not active for the + * current test + * @see #isActive() + * @see #isFlaggedForRollback() + * @see #start() + * @see #end() + */ + public static void flagForCommit() { + setFlaggedForRollback(false); + } + + /** + * Start a new test-managed transaction. + *
Only call this method if {@link #end} has been called or if no + * transaction has been previously started. + * @throws IllegalStateException if the transaction context could not be + * retrieved or if a transaction is already active for the current test + * @see #isActive() + * @see #end() + */ + public static void start() { + requireCurrentTransactionContext().startTransaction(); + } + + /** + * Immediately force a commit or rollback of the current + * test-managed transaction, according to the {@linkplain #isFlaggedForRollback + * rollback flag}. + * @throws IllegalStateException if the transaction context could not be + * retrieved or if a transaction is not active for the current test + * @see #isActive() + * @see #start() + */ + public static void end() { + requireCurrentTransactionContext().endTransaction(); + } + + private static TransactionContext requireCurrentTransactionContext() { + TransactionContext txContext = TransactionContextHolder.getCurrentTransactionContext(); + if (txContext == null) { + throw new IllegalStateException("TransactionContext is not active"); + } + return txContext; + } + + private static void setFlaggedForRollback(boolean flag) { + requireCurrentTransactionContext().setFlaggedForRollback(flag); + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java new file mode 100644 index 0000000000..f04b0dd776 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java @@ -0,0 +1,140 @@ +/* + * 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; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.test.context.TestContext; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + +/** + * Transaction context for a specific {@link TestContext}. + * + * @author Sam Brannen + * @author Juergen Hoeller + * @since 4.1 + * @see org.springframework.transaction.annotation.Transactional + * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener + */ +class TransactionContext { + + private static final Log logger = LogFactory.getLog(TransactionContext.class); + + private final TestContext testContext; + + private final TransactionDefinition transactionDefinition; + + private final PlatformTransactionManager transactionManager; + + private final boolean defaultRollback; + + private boolean flaggedForRollback; + + private TransactionStatus transactionStatus; + + private volatile int transactionsStarted = 0; + + + TransactionContext(TestContext testContext, PlatformTransactionManager transactionManager, + TransactionDefinition transactionDefinition, boolean defaultRollback) { + this.testContext = testContext; + this.transactionManager = transactionManager; + this.transactionDefinition = transactionDefinition; + this.defaultRollback = defaultRollback; + this.flaggedForRollback = defaultRollback; + } + + TransactionStatus getTransactionStatus() { + return this.transactionStatus; + } + + /** + * Has the current transaction been flagged for rollback? + *
In other words, should we roll back or commit the current transaction + * upon completion of the current test? + */ + boolean isFlaggedForRollback() { + return this.flaggedForRollback; + } + + void setFlaggedForRollback(boolean flaggedForRollback) { + if (this.transactionStatus == null) { + throw new IllegalStateException(String.format( + "Failed to set rollback flag for test context %s: transaction does not exist.", this.testContext)); + } + this.flaggedForRollback = flaggedForRollback; + } + + /** + * Start a new transaction for the configured {@linkplain #getTestContext test context}. + *
Only call this method if {@link #endTransaction} has been called or if no
+ * transaction has been previously started.
+ * @throws TransactionException if starting the transaction fails
+ */
+ void startTransaction() {
+ if (this.transactionStatus != null) {
+ throw new IllegalStateException(
+ "Cannot start a new transaction without ending the existing transaction first.");
+ }
+ this.flaggedForRollback = this.defaultRollback;
+ this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
+ ++this.transactionsStarted;
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format(
+ "Began transaction (%s) for test context %s; transaction manager [%s]; rollback [%s]",
+ this.transactionsStarted, this.testContext, this.transactionManager, flaggedForRollback));
+ }
+ }
+
+ /**
+ * Immediately force a commit or rollback of the transaction
+ * for the configured {@linkplain #getTestContext test context}, according to
+ * the {@linkplain #isFlaggedForRollback rollback flag}.
+ */
+ void endTransaction() {
+ if (logger.isTraceEnabled()) {
+ logger.trace(String.format(
+ "Ending transaction for test context %s; transaction status [%s]; rollback [%s]", this.testContext,
+ this.transactionStatus, flaggedForRollback));
+ }
+ if (this.transactionStatus == null) {
+ throw new IllegalStateException(String.format(
+ "Failed to end transaction for test context %s: transaction does not exist.", this.testContext));
+ }
+
+ try {
+ if (flaggedForRollback) {
+ this.transactionManager.rollback(this.transactionStatus);
+ }
+ else {
+ this.transactionManager.commit(this.transactionStatus);
+ }
+ }
+ finally {
+ this.transactionStatus = null;
+ }
+
+ if (logger.isInfoEnabled()) {
+ logger.info(String.format("%s transaction after test execution for test context %s.",
+ (flaggedForRollback ? "Rolled back" : "Committed"), this.testContext));
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java
new file mode 100644
index 0000000000..49c1d78be3
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+import org.springframework.core.NamedInheritableThreadLocal;
+
+/**
+ * {@link InheritableThreadLocal}-based holder for the current {@link TransactionContext}.
+ *
+ * @author Sam Brannen
+ * @since 4.1
+ */
+class TransactionContextHolder {
+
+ private static final ThreadLocal Note that if a {@code @BeforeTransaction} method fails, any remaining
* {@code @BeforeTransaction} methods will not be invoked, and a transaction
@@ -129,9 +122,9 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
final Class> testClass = testContext.getTestClass();
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
- if (this.transactionContextCache.remove(testMethod) != null) {
- throw new IllegalStateException("Cannot start new transaction without ending existing transaction: "
- + "Invoke endTransaction() before startNewTransaction().");
+ TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
+ if (txContext != null) {
+ throw new IllegalStateException("Cannot start a new transaction without ending the existing transaction.");
}
PlatformTransactionManager tm = null;
@@ -154,30 +147,34 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
}
if (tm != null) {
- TransactionContext txContext = new TransactionContext(tm, transactionAttribute);
+ txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext));
runBeforeTransactionMethods(testContext);
- startNewTransaction(testContext, txContext);
- this.transactionContextCache.put(testMethod, txContext);
+ txContext.startTransaction();
+ TransactionContextHolder.setCurrentTransactionContext(txContext);
}
}
/**
- * If a transaction is currently active for the test method of the supplied
- * {@link TestContext test context}, this method will end the transaction
- * and run {@link AfterTransaction @AfterTransaction methods}.
- * {@code @AfterTransaction} methods are guaranteed to be
- * invoked even if an error occurs while ending the transaction.
+ * If a transaction is currently active for the supplied
+ * {@linkplain TestContext test context}, this method will end the transaction
+ * and run {@link AfterTransaction @AfterTransaction} methods.
+ * {@code @AfterTransaction} methods are guaranteed to be invoked even if
+ * an error occurs while ending the transaction.
*/
@Override
public void afterTestMethod(TestContext testContext) throws Exception {
Method testMethod = testContext.getTestMethod();
Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
- // If the transaction is still active...
- TransactionContext txContext = this.transactionContextCache.remove(testMethod);
- if (txContext != null && !txContext.transactionStatus.isCompleted()) {
+ TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
+ // If there was (or perhaps still is) a transaction...
+ if (txContext != null) {
+ TransactionStatus transactionStatus = txContext.getTransactionStatus();
try {
- endTransaction(testContext, txContext);
+ // If the transaction is still active...
+ if ((transactionStatus != null) && !transactionStatus.isCompleted()) {
+ txContext.endTransaction();
+ }
}
finally {
runAfterTransactionMethods(testContext);
@@ -186,7 +183,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
}
/**
- * Run all {@link BeforeTransaction @BeforeTransaction methods} for the
+ * Run all {@link BeforeTransaction @BeforeTransaction} methods for the
* specified {@link TestContext test context}. If one of the methods fails,
* however, the caught exception will be rethrown in a wrapped
* {@link RuntimeException}, and the remaining methods will not
@@ -212,7 +209,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
}
/**
- * Run all {@link AfterTransaction @AfterTransaction methods} for the
+ * Run all {@link AfterTransaction @AfterTransaction} methods for the
* specified {@link TestContext test context}. If one of the methods fails,
* the caught exception will be logged as an error, and the remaining
* methods will be given a chance to execute. After all methods have
@@ -252,45 +249,6 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
}
}
- /**
- * Start a new transaction for the supplied {@link TestContext test context}.
- * Only call this method if {@link #endTransaction} has been called or if no
- * transaction has been previously started.
- * @param testContext the current test context
- * @throws TransactionException if starting the transaction fails
- * @throws Exception if an error occurs while retrieving the transaction manager
- */
- private void startNewTransaction(TestContext testContext, TransactionContext txContext) throws Exception {
- txContext.startTransaction();
- ++this.transactionsStarted;
- if (logger.isInfoEnabled()) {
- logger.info(String.format(
- "Began transaction (%s) for test context %s; transaction manager [%s]; rollback [%s]",
- this.transactionsStarted, testContext, txContext.transactionManager, isRollback(testContext)));
- }
- }
-
- /**
- * Immediately force a commit or rollback of the
- * transaction for the supplied {@link TestContext test context}, according
- * to the commit and rollback flags.
- * @param testContext the current test context
- * @throws Exception if an error occurs while retrieving the transaction manager
- */
- private void endTransaction(TestContext testContext, TransactionContext txContext) throws Exception {
- boolean rollback = isRollback(testContext);
- if (logger.isTraceEnabled()) {
- logger.trace(String.format(
- "Ending transaction for test context %s; transaction status [%s]; rollback [%s]", testContext,
- txContext.transactionStatus, rollback));
- }
- txContext.endTransaction(rollback);
- if (logger.isInfoEnabled()) {
- logger.info((rollback ? "Rolled back" : "Committed")
- + " transaction after test execution for test context " + testContext);
- }
- }
-
/**
* Get the {@link PlatformTransactionManager transaction manager} to use
* for the supplied {@linkplain TestContext test context} and {@code qualifier}.
@@ -478,7 +436,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
/**
* Retrieves the {@link TransactionConfigurationAttributes} for the
* specified {@link Class class} which may optionally declare or inherit
- * {@link TransactionConfiguration @TransactionConfiguration}. If
+ * {@link TransactionConfiguration @TransactionConfiguration}. If
* {@code @TransactionConfiguration} is not present for the supplied
* class, the default values for attributes defined in
* {@code @TransactionConfiguration} will be used instead.
@@ -520,37 +478,4 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis
return this.configurationAttributes;
}
-
- /**
- * Internal context holder for a specific test method.
- */
- private static class TransactionContext {
-
- private final PlatformTransactionManager transactionManager;
-
- private final TransactionDefinition transactionDefinition;
-
- private TransactionStatus transactionStatus;
-
-
- public TransactionContext(PlatformTransactionManager transactionManager,
- TransactionDefinition transactionDefinition) {
- this.transactionManager = transactionManager;
- this.transactionDefinition = transactionDefinition;
- }
-
- public void startTransaction() {
- this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
- }
-
- public void endTransaction(boolean rollback) {
- if (rollback) {
- this.transactionManager.rollback(this.transactionStatus);
- }
- else {
- this.transactionManager.commit(this.transactionStatus);
- }
- }
- }
-
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/EmptyDatabaseConfig.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/EmptyDatabaseConfig.java
index 90d5b34fe7..d6abe5b8d0 100644
--- a/spring-test/src/test/java/org/springframework/test/context/jdbc/EmptyDatabaseConfig.java
+++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/EmptyDatabaseConfig.java
@@ -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();
}
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/jdbc/PopulatedSchemaDatabaseConfig.java b/spring-test/src/test/java/org/springframework/test/context/jdbc/PopulatedSchemaDatabaseConfig.java
index 1c9573dae1..35e88ba297 100644
--- a/spring-test/src/test/java/org/springframework/test/context/jdbc/PopulatedSchemaDatabaseConfig.java
+++ b/spring-test/src/test/java/org/springframework/test/context/jdbc/PopulatedSchemaDatabaseConfig.java
@@ -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();
}
diff --git a/spring-test/src/test/java/org/springframework/test/context/transaction/TransactionalTestExecutionListenerTests.java b/spring-test/src/test/java/org/springframework/test/context/transaction/TransactionalTestExecutionListenerTests.java
index 71b30a465b..e2b2df6936 100644
--- a/spring-test/src/test/java/org/springframework/test/context/transaction/TransactionalTestExecutionListenerTests.java
+++ b/spring-test/src/test/java/org/springframework/test/context/transaction/TransactionalTestExecutionListenerTests.java
@@ -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
diff --git a/spring-test/src/test/java/org/springframework/test/context/transaction/programmatic/ProgrammaticTxMgmtTests.java b/spring-test/src/test/java/org/springframework/test/context/transaction/programmatic/ProgrammaticTxMgmtTests.java
new file mode 100644
index 0000000000..5e0f4d25a1
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/transaction/programmatic/ProgrammaticTxMgmtTests.java
@@ -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 Spring TestContext Framework.
+ *
+ * @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