All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.test.context.transaction.TransactionalTestExecutionListener Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2018 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
 *
 *      https://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.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

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.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.test.annotation.Commit;
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.TransactionStatus;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
 * {@code TestExecutionListener} that provides support for executing tests
 * within test-managed transactions by honoring Spring's
 * {@link org.springframework.transaction.annotation.Transactional @Transactional}
 * annotation.
 *
 * 

Test-managed Transactions

*

Test-managed transactions are transactions that are managed * declaratively via this listener or programmatically via * {@link TestTransaction}. Such transactions should not be confused with * Spring-managed transactions (i.e., those managed directly * by Spring within the {@code ApplicationContext} loaded for tests) or * application-managed transactions (i.e., those managed * programmatically within application code that is invoked via tests). * Spring-managed and application-managed transactions will typically * participate in test-managed transactions; however, caution should be * taken if Spring-managed or application-managed transactions are * configured with any propagation type other than * {@link org.springframework.transaction.annotation.Propagation#REQUIRED REQUIRED} * or {@link org.springframework.transaction.annotation.Propagation#SUPPORTS SUPPORTS}. * *

Enabling and Disabling Transactions

*

Annotating a test method with {@code @Transactional} causes the test * to be run within a transaction that will, by default, be automatically * rolled back after completion of the test. If a test class is * annotated with {@code @Transactional}, each test method within that class * hierarchy will be run within a transaction. Test methods that are * not annotated with {@code @Transactional} (at the class or method * level) will not be run within a transaction. Furthermore, tests that * are annotated with {@code @Transactional} but have the * {@link org.springframework.transaction.annotation.Transactional#propagation propagation} * type set to * {@link org.springframework.transaction.annotation.Propagation#NOT_SUPPORTED NOT_SUPPORTED} * will not be run within a transaction. * *

Declarative Rollback and Commit Behavior

*

By default, test transactions will be automatically rolled back * after completion of the test; however, transactional commit and rollback * behavior can be configured declaratively via the {@link Commit @Commit} * and {@link Rollback @Rollback} annotations at the class level and at the * method level. * *

Programmatic Transaction Management

*

As of Spring Framework 4.1, it is possible to interact with test-managed * transactions programmatically via the static methods in {@link TestTransaction}. * {@code TestTransaction} may be used within test methods, * before methods, and after methods. * *

Executing Code outside of a Transaction

*

When executing transactional tests, it is sometimes useful to be able to * execute certain set up or tear down code outside of a * transaction. {@code TransactionalTestExecutionListener} provides such * support for methods annotated with {@link BeforeTransaction @BeforeTransaction} * or {@link AfterTransaction @AfterTransaction}. As of Spring Framework 4.3, * {@code @BeforeTransaction} and {@code @AfterTransaction} may also be declared * on Java 8 based interface default methods. * *

Configuring a Transaction Manager

*

{@code TransactionalTestExecutionListener} expects a * {@link PlatformTransactionManager} bean to be defined in the Spring * {@code ApplicationContext} for the test. In case there are multiple * instances of {@code PlatformTransactionManager} within the test's * {@code ApplicationContext}, a qualifier may be declared via * {@link org.springframework.transaction.annotation.Transactional @Transactional} * (e.g., {@code @Transactional("myTxMgr")} or {@code @Transactional(transactionManger = "myTxMgr")}, * or {@link org.springframework.transaction.annotation.TransactionManagementConfigurer * TransactionManagementConfigurer} can be implemented by an * {@link org.springframework.context.annotation.Configuration @Configuration} * class. See {@link TestContextTransactionUtils#retrieveTransactionManager} * for details on the algorithm used to look up a transaction manager in * the test's {@code ApplicationContext}. * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 * @see org.springframework.transaction.annotation.TransactionManagementConfigurer * @see org.springframework.transaction.annotation.Transactional * @see org.springframework.test.annotation.Commit * @see org.springframework.test.annotation.Rollback * @see BeforeTransaction * @see AfterTransaction * @see TestTransaction */ public class TransactionalTestExecutionListener extends AbstractTestExecutionListener { private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class); // Do not require @Transactional test methods to be public. protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false); /** * Returns {@code 4000}. */ @Override public final int getOrder() { return 4000; } /** * 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 * will not be started. * @see org.springframework.transaction.annotation.Transactional * @see #getTransactionManager(TestContext, String) */ @Override public void beforeTestMethod(final TestContext testContext) throws Exception { Method testMethod = testContext.getTestMethod(); Class testClass = testContext.getTestClass(); Assert.notNull(testMethod, "Test method of supplied TestContext must not be null"); TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext(); Assert.state(txContext == null, "Cannot start new transaction without ending existing transaction"); PlatformTransactionManager tm = null; TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, testClass); if (transactionAttribute != null) { transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute(testContext, transactionAttribute); if (logger.isDebugEnabled()) { logger.debug("Explicit transaction definition [" + transactionAttribute + "] found for test context " + testContext); } if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { return; } tm = getTransactionManager(testContext, transactionAttribute.getQualifier()); Assert.state(tm != null, () -> "Failed to retrieve PlatformTransactionManager for @Transactional test: " + testContext); } if (tm != null) { txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext)); runBeforeTransactionMethods(testContext); txContext.startTransaction(); TransactionContextHolder.setCurrentTransactionContext(txContext); } } /** * 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"); TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext(); // If there was (or perhaps still is) a transaction... if (txContext != null) { TransactionStatus transactionStatus = txContext.getTransactionStatus(); try { // If the transaction is still active... if (transactionStatus != null && !transactionStatus.isCompleted()) { txContext.endTransaction(); } } finally { runAfterTransactionMethods(testContext); } } } /** * Run all {@link BeforeTransaction @BeforeTransaction} methods for the * specified {@linkplain 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 methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class); Collections.reverse(methods); for (Method method : methods) { if (logger.isDebugEnabled()) { logger.debug("Executing @BeforeTransaction method [" + method + "] for test context " + testContext); } ReflectionUtils.makeAccessible(method); method.invoke(testContext.getTestInstance()); } } catch (InvocationTargetException ex) { if (logger.isErrorEnabled()) { logger.error("Exception encountered while executing @BeforeTransaction methods for test context " + testContext + ".", ex.getTargetException()); } ReflectionUtils.rethrowException(ex.getTargetException()); } } /** * Run all {@link AfterTransaction @AfterTransaction} methods for the * specified {@linkplain 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 * executed, the first caught exception, if any, will be rethrown. * @param testContext the current test context */ protected void runAfterTransactionMethods(TestContext testContext) throws Exception { Throwable afterTransactionException = null; List methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class); for (Method method : methods) { try { if (logger.isDebugEnabled()) { logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext); } ReflectionUtils.makeAccessible(method); method.invoke(testContext.getTestInstance()); } catch (InvocationTargetException ex) { Throwable targetException = ex.getTargetException(); if (afterTransactionException == null) { afterTransactionException = targetException; } logger.error("Exception encountered while executing @AfterTransaction method [" + method + "] for test context " + testContext, targetException); } catch (Exception ex) { if (afterTransactionException == null) { afterTransactionException = ex; } logger.error("Exception encountered while executing @AfterTransaction method [" + method + "] for test context " + testContext, ex); } } if (afterTransactionException != null) { ReflectionUtils.rethrowException(afterTransactionException); } } /** * Get the {@linkplain PlatformTransactionManager transaction manager} to use * for the supplied {@linkplain TestContext test context} and {@code qualifier}. *

Delegates to {@link #getTransactionManager(TestContext)} if the * supplied {@code qualifier} is {@code null} or empty. * @param testContext the test context for which the transaction manager * should be retrieved * @param qualifier the qualifier for selecting between multiple bean matches; * may be {@code null} or empty * @return the transaction manager to use, or {@code null} if not found * @throws BeansException if an error occurs while retrieving the transaction manager * @see #getTransactionManager(TestContext) */ @Nullable protected PlatformTransactionManager getTransactionManager(TestContext testContext, @Nullable String qualifier) { // Look up by type and qualifier from @Transactional if (StringUtils.hasText(qualifier)) { try { // 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(); return BeanFactoryAnnotationUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier); } catch (RuntimeException ex) { if (logger.isWarnEnabled()) { logger.warn(String.format( "Caught exception while retrieving transaction manager with qualifier '%s' for test context %s", qualifier, testContext), ex); } throw ex; } } // else return getTransactionManager(testContext); } /** * Get the {@linkplain PlatformTransactionManager transaction manager} * to use for the supplied {@linkplain TestContext test context}. *

The default implementation simply delegates to * {@link TestContextTransactionUtils#retrieveTransactionManager}. * @param testContext the test context for which the transaction manager * should be retrieved * @return the transaction manager to use, or {@code null} if not found * @throws BeansException if an error occurs while retrieving an explicitly * named transaction manager * @throws IllegalStateException if more than one TransactionManagementConfigurer * exists in the ApplicationContext * @see #getTransactionManager(TestContext, String) */ @Nullable protected PlatformTransactionManager getTransactionManager(TestContext testContext) { return TestContextTransactionUtils.retrieveTransactionManager(testContext, null); } /** * Determine whether or not to rollback transactions by default for the * supplied {@linkplain TestContext test context}. *

Supports {@link Rollback @Rollback} or {@link Commit @Commit} at the * class-level. * @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 { Class testClass = testContext.getTestClass(); Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class); boolean rollbackPresent = (rollback != null); if (rollbackPresent) { boolean defaultRollback = rollback.value(); if (logger.isDebugEnabled()) { logger.debug(String.format("Retrieved default @Rollback(%s) for test class [%s].", defaultRollback, testClass.getName())); } return defaultRollback; } // else return true; } /** * Determine whether or not to rollback transactions for the supplied * {@linkplain TestContext test context} by taking into consideration the * {@linkplain #isDefaultRollback(TestContext) default rollback} flag and a * possible method-level override via the {@link Rollback @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 = AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), Rollback.class); if (rollbackAnnotation != null) { boolean rollbackOverride = rollbackAnnotation.value(); if (logger.isDebugEnabled()) { logger.debug(String.format( "Method-level @Rollback(%s) overrides default rollback [%s] for test context %s.", rollbackOverride, rollback, testContext)); } rollback = rollbackOverride; } else { if (logger.isDebugEnabled()) { logger.debug(String.format( "No method-level @Rollback override: using default rollback [%s] for test context %s.", rollback, testContext)); } } return rollback; } /** * Get all methods in the supplied {@link Class class} and its superclasses * which are annotated with the supplied {@code annotationType} but * which are not shadowed by methods overridden in subclasses. *

Default methods on interfaces are also detected. * @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 * as well as annotated interface default methods */ private List getAnnotatedMethods(Class clazz, Class annotationType) { return Arrays.stream(ReflectionUtils.getUniqueDeclaredMethods(clazz)) .filter(method -> AnnotatedElementUtils.hasAnnotation(method, annotationType)) .collect(Collectors.toList()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy