Java tutorial
/* * Copyright 2010 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 com.github.springtestdbunit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.TestContext; import org.springframework.test.context.transaction.AfterTransaction; import org.springframework.test.context.transaction.BeforeTransaction; import org.springframework.test.context.transaction.TransactionConfiguration; import org.springframework.test.context.transaction.TransactionConfigurationAttributes; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; 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.SpringTransactionAnnotationParser; import org.springframework.transaction.annotation.TransactionAnnotationParser; import org.springframework.transaction.interceptor.DelegatingTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAspectUtils; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.util.StringUtils; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.DatabaseTearDown; import com.github.springtestdbunit.annotation.ExpectedDatabase; /** * <p> * <code>TestExecutionListener</code> which provides support for executing * tests within single transaction per test class against predefined dataset. * </p> * <p> * Such behavior is useful if there is a need to setup a dataset, perform a series * of read-only tests against this dataset and roll-back all changes eventually. * </p> * <p> * Database setup is controlled via the class-level {@link DatabaseSetup} annotation. * {@link DatabaseTearDown} and {@link ExpectedDatabase} annotations are * <b>not</b> supported * </p> * <p> * Transactional commit and rollback behavior can be configured via the * class-level {@link TransactionConfiguration @TransactionConfiguration} annotation. * {@link TransactionConfiguration @TransactionConfiguration} also provides * configuration of the bean name of the {@link PlatformTransactionManager} that * is to be used to drive transactions. {@link Rollback}, {@link BeforeTransaction} and * {@link AfterTransaction} annotation are <b>not</b> supported. * </p> * <p> * The code is highly based on Spring {@link TransactionalTestExecutionListener}. * </p> * * @author Dmytro Kostiuchenko * @see TransactionalTestExecutionListener * @see DatabaseSetup */ public class SingleTransactionTestExecutionListener extends AbstractDbUnitTestExecutionListener { private static final Log logger = LogFactory.getLog(SingleTransactionTestExecutionListener.class); private static DbUnitRunner runner = new DbUnitRunner(); private TransactionConfigurationAttributes configurationAttributes; private TransactionAnnotationParser annotationParser = new SpringTransactionAnnotationParser(); private volatile TransactionContext transactionContext; @Override public void prepareTestInstance(TestContext testContext) throws Exception { // skip test instance preparation. It occurs in beforeTestClass now } @SuppressWarnings("serial") @Override public void beforeTestClass(TestContext testContext) throws Exception { super.prepareTestInstance(testContext); final Class<?> testClass = testContext.getTestClass(); TransactionAttribute transactionAttribute = annotationParser.parseTransactionAnnotation(testClass); TransactionDefinition transactionDefinition = null; if (transactionAttribute != null) { transactionDefinition = new DelegatingTransactionAttribute(transactionAttribute) { public String getName() { return testClass.getName(); } }; } if (transactionDefinition != null) { if (logger.isDebugEnabled()) { logger.debug("Explicit transaction definition [" + transactionDefinition + "] found for test context [" + testContext + "]"); } String qualifier = transactionAttribute.getQualifier(); PlatformTransactionManager tm; if (StringUtils.hasLength(qualifier)) { // Use autowire-capable factory in order to support extended qualifier matching // (only exposed on the internal BeanFactory, not on the ApplicationContext). BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); tm = TransactionAspectUtils.getTransactionManager(bf, qualifier); } else { tm = getTransactionManager(testContext); } transactionContext = new TransactionContext(tm, transactionDefinition); startNewTransaction(testContext, transactionContext); logger.debug("Started transaction. Setting up database"); runner.beforeTestMethod(new DbUnitTestContextAdapter(testContext)); } } /** * 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}. * <p>{@link AfterTransaction @AfterTransaction methods} are guaranteed to be * invoked even if an error occurs while ending the transaction. */ @Override public void afterTestClass(TestContext testContext) throws Exception { // If the transaction is still active... TransactionContext txContext = transactionContext; if (txContext != null && !txContext.transactionStatus.isCompleted()) { endTransaction(testContext, txContext); } } /** * Start a new transaction for the supplied {@link TestContext test context}. * <p>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(); if (logger.isInfoEnabled()) { logger.info("Began transaction : transaction manager [" + txContext.transactionManager + "]; rollback [" + isRollback(testContext) + "]"); } } /** * Immediately force a <em>commit</em> or <em>rollback</em> 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 <code>null</code> if not found * @throws BeansException if an error occurs while retrieving the transaction manager */ protected final PlatformTransactionManager getTransactionManager(TestContext testContext) { String tmName = retrieveConfigurationAttributes(testContext).getTransactionManagerName(); try { return testContext.getApplicationContext().getBean(tmName, PlatformTransactionManager.class); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Caught exception while retrieving transaction manager with bean name [" + tmName + "] 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 <em>default rollback</em> flag for the supplied test context * @throws Exception if an error occurs while determining the default rollback flag */ protected final boolean isRollback(TestContext testContext) throws Exception { return retrieveConfigurationAttributes(testContext).isDefaultRollback(); } /** * 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 <em>default values</em> 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 retrieveConfigurationAttributes(TestContext testContext) { if (this.configurationAttributes == null) { Class<?> clazz = testContext.getTestClass(); Class<TransactionConfiguration> annotationType = TransactionConfiguration.class; TransactionConfiguration config = clazz.getAnnotation(annotationType); if (logger.isDebugEnabled()) { logger.debug("Retrieved @TransactionConfiguration [" + config + "] for test class [" + clazz + "]"); } String transactionManagerName; boolean defaultRollback; if (config != null) { transactionManagerName = config.transactionManager(); defaultRollback = config.defaultRollback(); } else { transactionManagerName = (String) AnnotationUtils.getDefaultValue(annotationType, "transactionManager"); defaultRollback = (Boolean) AnnotationUtils.getDefaultValue(annotationType, "defaultRollback"); } TransactionConfigurationAttributes configAttributes = new TransactionConfigurationAttributes( transactionManagerName, defaultRollback); if (logger.isDebugEnabled()) { logger.debug("Retrieved TransactionConfigurationAttributes [" + configAttributes + "] for class [" + clazz + "]"); } this.configurationAttributes = configAttributes; } 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); } } } }