com.github.springtestdbunit.SingleTransactionTestExecutionListener.java Source code

Java tutorial

Introduction

Here is the source code for com.github.springtestdbunit.SingleTransactionTestExecutionListener.java

Source

/*
 * 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);
            }
        }
    }

}