From f667e43ca2fc3ffdc4f80fa604fb798b22b64dad Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 24 Jun 2014 13:48:20 +0200 Subject: [PATCH] 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 --- .../context/transaction/TestTransaction.java | 146 +++++++++ .../transaction/TransactionContext.java | 140 +++++++++ .../transaction/TransactionContextHolder.java | 49 +++ .../TransactionalTestExecutionListener.java | 129 ++------ .../context/jdbc/EmptyDatabaseConfig.java | 4 +- .../jdbc/PopulatedSchemaDatabaseConfig.java | 1 + ...ansactionalTestExecutionListenerTests.java | 18 +- .../programmatic/ProgrammaticTxMgmtTests.java | 284 ++++++++++++++++++ 8 files changed, 663 insertions(+), 108 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/transaction/TestTransaction.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/transaction/programmatic/ProgrammaticTxMgmtTests.java 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 currentTransactionContext = new NamedInheritableThreadLocal( + "Test Transaction Context"); + + + static TransactionContext getCurrentTransactionContext() { + return currentTransactionContext.get(); + } + + static void setCurrentTransactionContext(TransactionContext transactionContext) { + currentTransactionContext.set(transactionContext); + } + + static TransactionContext removeCurrentTransactionContext() { + synchronized (currentTransactionContext) { + TransactionContext transactionContext = currentTransactionContext.get(); + currentTransactionContext.remove(); + return transactionContext; + } + } + +} \ No newline at end of file diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 8c6cf27280..8e7bf5e37f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -22,8 +22,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -38,7 +36,6 @@ import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; import org.springframework.transaction.annotation.TransactionManagementConfigurer; @@ -53,7 +50,7 @@ import static org.springframework.core.annotation.AnnotationUtils.*; /** * {@code TestExecutionListener} that provides support for executing tests * within transactions by honoring the - * {@link org.springframework.transaction.annotation.Transactional @Transactional} + * {@link org.springframework.transaction.annotation.Transactional @Transactional} * annotation. Expects a {@link PlatformTransactionManager} bean to be defined in the * Spring {@link ApplicationContext} for the test. * @@ -91,6 +88,7 @@ import static org.springframework.core.annotation.AnnotationUtils.*; * @see org.springframework.test.annotation.Rollback * @see BeforeTransaction * @see AfterTransaction + * @see TestTransaction */ public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { @@ -104,18 +102,13 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(); - private final Map transactionContextCache = new ConcurrentHashMap( - 8); - private TransactionConfigurationAttributes configurationAttributes; - private volatile int transactionsStarted = 0; - /** - * If the test method of the supplied {@link TestContext test context} is - * configured to run within a transaction, this method will run - * {@link BeforeTransaction @BeforeTransaction methods} and start a new + * If the test method of the supplied {@linkplain TestContext test context} + * is configured to run within a transaction, this method will run + * {@link BeforeTransaction @BeforeTransaction} methods and start a new * transaction. *

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> results = jdbcTemplate.queryForList("select name from user"); + List names = new ArrayList(); + for (Map 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(); + } + } + +}