/* * Copyright 2002-2009 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 java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.test.annotation.NotTransactional; import org.springframework.test.annotation.Rollback; 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.interceptor.DelegatingTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttributeSource; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** *
* TestExecutionListener
which provides support for executing
* tests within transactions by using
* {@link org.springframework.transaction.annotation.Transactional @Transactional}
* and {@link NotTransactional @NotTransactional} annotations.
*
* Changes to the database during a test run with @Transactional will be * run within a transaction that will, by default, be automatically * rolled back after completion of the test; whereas, changes to the * database during a test run with @NotTransactional will not * be run within a transaction. Similarly, test methods that are not annotated * with either @Transactional (at the class or method level) or * @NotTransactional will not be run within a transaction. *
** Transactional commit and rollback behavior can be configured via the * class-level {@link TransactionConfiguration @TransactionConfiguration} and * method-level {@link Rollback @Rollback} annotations. * {@link TransactionConfiguration @TransactionConfiguration} also provides * configuration of the bean name of the {@link PlatformTransactionManager} that * is to be used to drive transactions. *
*
* When executing transactional tests, it is sometimes useful to be able execute
* certain set up or tear down code outside of a
* transaction. TransactionalTestExecutionListener
provides such
* support for methods annotated with
* {@link BeforeTransaction @BeforeTransaction} and
* {@link AfterTransaction @AfterTransaction}.
*
Note that if a {@link BeforeTransaction @BeforeTransaction method} fails, * remaining {@link BeforeTransaction @BeforeTransaction methods} will not * be invoked, and a transaction will not be started. * @see org.springframework.transaction.annotation.Transactional * @see org.springframework.test.annotation.NotTransactional */ @SuppressWarnings("serial") @Override public void beforeTestMethod(TestContext testContext) throws Exception { final Method testMethod = testContext.getTestMethod(); 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()."); } if (testMethod.isAnnotationPresent(NotTransactional.class)) { return; } TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, testContext.getTestClass()); TransactionDefinition transactionDefinition = null; if (transactionAttribute != null) { transactionDefinition = new DelegatingTransactionAttribute(transactionAttribute) { public String getName() { return testMethod.getName(); } }; } if (transactionDefinition != null) { if (logger.isDebugEnabled()) { logger.debug("Explicit transaction definition [" + transactionDefinition + "] found for test context [" + testContext + "]"); } TransactionContext txContext = new TransactionContext(getTransactionManager(testContext), transactionDefinition); runBeforeTransactionMethods(testContext); startNewTransaction(testContext, txContext); this.transactionContextCache.put(testMethod, 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}. *
{@link AfterTransaction @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()) {
try {
endTransaction(testContext, txContext);
}
finally {
runAfterTransactionMethods(testContext);
}
}
}
/**
* 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
* be given a chance to execute.
* @param testContext the current test context
*/
protected void runBeforeTransactionMethods(TestContext testContext) throws Exception {
try {
List 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("Began transaction (" + this.transactionsStarted + "): transaction manager [" +
txContext.transactionManager + "]; rollback [" + 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("Ending transaction for test context [" + testContext + "]; transaction manager [" +
txContext.transactionStatus + "]; rollback [" + 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 {@link TestContext test context}.
* @param testContext the test context for which the transaction manager
* should be retrieved
* @return the transaction manager to use, or Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#getSuperClasses(Class)} and
* adapted.
* @param clazz the class for which to retrieve the superclasses.
* @return all superclasses of the supplied class.
*/
private List Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#getAnnotatedMethods(Class)}
* and adapted.
* @param clazz the class for which to retrieve the annotated methods
* @param annotationType the annotation type for which to search
* @return all annotated methods in the supplied class and its superclasses
*/
private List Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#isShadowed(Method,List)}.
* @param method the method to check for shadowing
* @param previousMethods the list of methods which have previously been processed
* @return Note: This code has been borrowed from
* {@link org.junit.internal.runners.TestClass#isShadowed(Method,Method)}.
* @param current the current method
* @param previous the previous method
* @return
* Retrieves the {@link TransactionConfigurationAttributes} for the
* specified {@link Class class} which may optionally declare or inherit a
* {@link TransactionConfiguration @TransactionConfiguration}. If a
* {@link TransactionConfiguration} annotation is not present for the
* supplied class, the default values for attributes defined in
* {@link TransactionConfiguration} will be used instead.
* @param clazz the Class object corresponding to the test class for which
* the configuration attributes should be retrieved
* @return a new TransactionConfigurationAttributes instance
*/
private TransactionConfigurationAttributes retrieveTransactionConfigurationAttributes(Class> clazz) {
Classnull
if not found
* @throws BeansException if an error occurs while retrieving the transaction manager
*/
protected final PlatformTransactionManager getTransactionManager(TestContext testContext) {
if (this.configAttributes == null) {
this.configAttributes = retrieveTransactionConfigurationAttributes(testContext.getTestClass());
}
String transactionManagerName = this.configAttributes.getTransactionManagerName();
try {
return (PlatformTransactionManager) testContext.getApplicationContext().getBean(
transactionManagerName, PlatformTransactionManager.class);
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Caught exception while retrieving transaction manager with bean name [" +
transactionManagerName + "] for test context [" + testContext + "]", ex);
}
throw ex;
}
}
/**
* Determine whether or not to rollback transactions by default for the
* supplied {@link TestContext test context}.
* @param testContext the test context for which the default rollback flag
* should be retrieved
* @return the default rollback flag for the supplied test context
* @throws Exception if an error occurs while determining the default rollback flag
*/
protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
return retrieveTransactionConfigurationAttributes(testContext.getTestClass()).isDefaultRollback();
}
/**
* Determine whether or not to rollback transactions for the supplied
* {@link TestContext test context} by taking into consideration the
* {@link #isDefaultRollback(TestContext) default rollback} flag and a
* possible method-level override via the {@link Rollback} annotation.
* @param testContext the test context for which the rollback flag
* should be retrieved
* @return the rollback flag for the supplied test context
* @throws Exception if an error occurs while determining the rollback flag
*/
protected final boolean isRollback(TestContext testContext) throws Exception {
boolean rollback = isDefaultRollback(testContext);
Rollback rollbackAnnotation = testContext.getTestMethod().getAnnotation(Rollback.class);
if (rollbackAnnotation != null) {
boolean rollbackOverride = rollbackAnnotation.value();
if (logger.isDebugEnabled()) {
logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback [" +
rollback + "] for test context [" + testContext + "]");
}
rollback = rollbackOverride;
}
else {
if (logger.isDebugEnabled()) {
logger.debug("No method-level @Rollback override: using default rollback [" +
rollback + "] for test context [" + testContext + "]");
}
}
return rollback;
}
/**
* Gets all superclasses of the supplied {@link Class class}, including the
* class itself. The ordering of the returned list will begin with the
* supplied class and continue up the class hierarchy.
* annotationType
but
* which are not shadowed by methods overridden in subclasses.
* true
if the supplied method is shadowed by a
* method in the previousMethods
list
*/
private boolean isShadowed(Method method, Listtrue
if the previous method shadows the current one
*/
private boolean isShadowed(Method current, Method previous) {
if (!previous.getName().equals(current.getName())) {
return false;
}
if (previous.getParameterTypes().length != current.getParameterTypes().length) {
return false;
}
for (int i = 0; i < previous.getParameterTypes().length; i++) {
if (!previous.getParameterTypes()[i].equals(current.getParameterTypes()[i])) {
return false;
}
}
return true;
}
/**
*