001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.test.context.transaction;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.InvocationTargetException;
021import java.lang.reflect.Method;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.beans.BeansException;
030import org.springframework.beans.factory.BeanFactory;
031import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
032import org.springframework.core.annotation.AnnotatedElementUtils;
033import org.springframework.core.annotation.AnnotationUtils;
034import org.springframework.test.annotation.Commit;
035import org.springframework.test.annotation.Rollback;
036import org.springframework.test.context.TestContext;
037import org.springframework.test.context.support.AbstractTestExecutionListener;
038import org.springframework.transaction.PlatformTransactionManager;
039import org.springframework.transaction.TransactionDefinition;
040import org.springframework.transaction.TransactionStatus;
041import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
042import org.springframework.transaction.interceptor.TransactionAttribute;
043import org.springframework.transaction.interceptor.TransactionAttributeSource;
044import org.springframework.util.Assert;
045import org.springframework.util.ReflectionUtils;
046import org.springframework.util.StringUtils;
047
048/**
049 * {@code TestExecutionListener} that provides support for executing tests
050 * within <em>test-managed transactions</em> by honoring Spring's
051 * {@link org.springframework.transaction.annotation.Transactional @Transactional}
052 * annotation.
053 *
054 * <h3>Test-managed Transactions</h3>
055 * <p><em>Test-managed transactions</em> are transactions that are managed
056 * declaratively via this listener or programmatically via
057 * {@link TestTransaction}. Such transactions should not be confused with
058 * <em>Spring-managed transactions</em> (i.e., those managed directly
059 * by Spring within the {@code ApplicationContext} loaded for tests) or
060 * <em>application-managed transactions</em> (i.e., those managed
061 * programmatically within application code that is invoked via tests).
062 * Spring-managed and application-managed transactions will typically
063 * participate in test-managed transactions; however, caution should be
064 * taken if Spring-managed or application-managed transactions are
065 * configured with any propagation type other than
066 * {@link org.springframework.transaction.annotation.Propagation#REQUIRED REQUIRED}
067 * or {@link org.springframework.transaction.annotation.Propagation#SUPPORTS SUPPORTS}.
068 *
069 * <h3>Enabling and Disabling Transactions</h3>
070 * <p>Annotating a test method with {@code @Transactional} causes the test
071 * to be run within a transaction that will, by default, be automatically
072 * <em>rolled back</em> after completion of the test. If a test class is
073 * annotated with {@code @Transactional}, each test method within that class
074 * hierarchy will be run within a transaction. Test methods that are
075 * <em>not</em> annotated with {@code @Transactional} (at the class or method
076 * level) will not be run within a transaction. Furthermore, tests that
077 * <em>are</em> annotated with {@code @Transactional} but have the
078 * {@link org.springframework.transaction.annotation.Transactional#propagation propagation}
079 * type set to
080 * {@link org.springframework.transaction.annotation.Propagation#NOT_SUPPORTED NOT_SUPPORTED}
081 * will not be run within a transaction.
082 *
083 * <h3>Declarative Rollback and Commit Behavior</h3>
084 * <p>By default, test transactions will be automatically <em>rolled back</em>
085 * after completion of the test; however, transactional commit and rollback
086 * behavior can be configured declaratively via the {@link Commit @Commit}
087 * and {@link Rollback @Rollback} annotations at the class level and at the
088 * method level.
089 *
090 * <h3>Programmatic Transaction Management</h3>
091 * <p>As of Spring Framework 4.1, it is possible to interact with test-managed
092 * transactions programmatically via the static methods in {@link TestTransaction}.
093 * {@code TestTransaction} may be used within <em>test</em> methods,
094 * <em>before</em> methods, and <em>after</em> methods.
095 *
096 * <h3>Executing Code outside of a Transaction</h3>
097 * <p>When executing transactional tests, it is sometimes useful to be able to
098 * execute certain <em>set up</em> or <em>tear down</em> code outside of a
099 * transaction. {@code TransactionalTestExecutionListener} provides such
100 * support for methods annotated with {@link BeforeTransaction @BeforeTransaction}
101 * or {@link AfterTransaction @AfterTransaction}. As of Spring Framework 4.3,
102 * {@code @BeforeTransaction} and {@code @AfterTransaction} may also be declared
103 * on Java 8 based interface default methods.
104 *
105 * <h3>Configuring a Transaction Manager</h3>
106 * <p>{@code TransactionalTestExecutionListener} expects a
107 * {@link PlatformTransactionManager} bean to be defined in the Spring
108 * {@code ApplicationContext} for the test. In case there are multiple
109 * instances of {@code PlatformTransactionManager} within the test's
110 * {@code ApplicationContext}, a <em>qualifier</em> may be declared via
111 * {@link org.springframework.transaction.annotation.Transactional @Transactional}
112 * (e.g., {@code @Transactional("myTxMgr")} or {@code @Transactional(transactionManger = "myTxMgr")},
113 * or {@link org.springframework.transaction.annotation.TransactionManagementConfigurer
114 * TransactionManagementConfigurer} can be implemented by an
115 * {@link org.springframework.context.annotation.Configuration @Configuration}
116 * class. See {@link TestContextTransactionUtils#retrieveTransactionManager}
117 * for details on the algorithm used to look up a transaction manager in
118 * the test's {@code ApplicationContext}.
119 *
120 * @author Sam Brannen
121 * @author Juergen Hoeller
122 * @since 2.5
123 * @see org.springframework.transaction.annotation.TransactionManagementConfigurer
124 * @see org.springframework.transaction.annotation.Transactional
125 * @see org.springframework.test.annotation.Commit
126 * @see org.springframework.test.annotation.Rollback
127 * @see BeforeTransaction
128 * @see AfterTransaction
129 * @see TestTransaction
130 */
131public class TransactionalTestExecutionListener extends AbstractTestExecutionListener {
132
133        private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class);
134
135        @SuppressWarnings("deprecation")
136        private static final TransactionConfigurationAttributes defaultTxConfigAttributes = new TransactionConfigurationAttributes();
137
138        // Do not require @Transactional test methods to be public.
139        protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false);
140
141        @SuppressWarnings("deprecation")
142        private TransactionConfigurationAttributes configurationAttributes;
143
144
145        /**
146         * Returns {@code 4000}.
147         */
148        @Override
149        public final int getOrder() {
150                return 4000;
151        }
152
153        /**
154         * If the test method of the supplied {@linkplain TestContext test context}
155         * is configured to run within a transaction, this method will run
156         * {@link BeforeTransaction @BeforeTransaction} methods and start a new
157         * transaction.
158         * <p>Note that if a {@code @BeforeTransaction} method fails, any remaining
159         * {@code @BeforeTransaction} methods will not be invoked, and a transaction
160         * will not be started.
161         * @see org.springframework.transaction.annotation.Transactional
162         * @see #getTransactionManager(TestContext, String)
163         */
164        @Override
165        public void beforeTestMethod(final TestContext testContext) throws Exception {
166                Method testMethod = testContext.getTestMethod();
167                Class<?> testClass = testContext.getTestClass();
168                Assert.notNull(testMethod, "Test method of supplied TestContext must not be null");
169
170                TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
171                Assert.state(txContext == null, "Cannot start new transaction without ending existing transaction");
172
173                PlatformTransactionManager tm = null;
174                TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, testClass);
175
176                if (transactionAttribute != null) {
177                        transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute(testContext,
178                                transactionAttribute);
179
180                        if (logger.isDebugEnabled()) {
181                                logger.debug("Explicit transaction definition [" + transactionAttribute +
182                                                "] found for test context " + testContext);
183                        }
184
185                        if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
186                                return;
187                        }
188
189                        tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
190
191                        if (tm == null) {
192                                throw new IllegalStateException(
193                                                "Failed to retrieve PlatformTransactionManager for @Transactional test: " + testContext);
194                        }
195                }
196
197                if (tm != null) {
198                        txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext));
199                        runBeforeTransactionMethods(testContext);
200                        txContext.startTransaction();
201                        TransactionContextHolder.setCurrentTransactionContext(txContext);
202                }
203        }
204
205        /**
206         * If a transaction is currently active for the supplied
207         * {@linkplain TestContext test context}, this method will end the transaction
208         * and run {@link AfterTransaction @AfterTransaction} methods.
209         * <p>{@code @AfterTransaction} methods are guaranteed to be invoked even if
210         * an error occurs while ending the transaction.
211         */
212        @Override
213        public void afterTestMethod(TestContext testContext) throws Exception {
214                Method testMethod = testContext.getTestMethod();
215                Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
216
217                TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
218                // If there was (or perhaps still is) a transaction...
219                if (txContext != null) {
220                        TransactionStatus transactionStatus = txContext.getTransactionStatus();
221                        try {
222                                // If the transaction is still active...
223                                if (transactionStatus != null && !transactionStatus.isCompleted()) {
224                                        txContext.endTransaction();
225                                }
226                        }
227                        finally {
228                                runAfterTransactionMethods(testContext);
229                        }
230                }
231        }
232
233        /**
234         * Run all {@link BeforeTransaction @BeforeTransaction} methods for the
235         * specified {@linkplain TestContext test context}. If one of the methods
236         * fails, however, the caught exception will be rethrown in a wrapped
237         * {@link RuntimeException}, and the remaining methods will <strong>not</strong>
238         * be given a chance to execute.
239         * @param testContext the current test context
240         */
241        protected void runBeforeTransactionMethods(TestContext testContext) throws Exception {
242                try {
243                        List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class);
244                        Collections.reverse(methods);
245                        for (Method method : methods) {
246                                if (logger.isDebugEnabled()) {
247                                        logger.debug("Executing @BeforeTransaction method [" + method + "] for test context " + testContext);
248                                }
249                                ReflectionUtils.makeAccessible(method);
250                                method.invoke(testContext.getTestInstance());
251                        }
252                }
253                catch (InvocationTargetException ex) {
254                        if (logger.isErrorEnabled()) {
255                                logger.error("Exception encountered while executing @BeforeTransaction methods for test context " +
256                                                testContext + ".", ex.getTargetException());
257                        }
258                        ReflectionUtils.rethrowException(ex.getTargetException());
259                }
260        }
261
262        /**
263         * Run all {@link AfterTransaction @AfterTransaction} methods for the
264         * specified {@linkplain TestContext test context}. If one of the methods
265         * fails, the caught exception will be logged as an error, and the remaining
266         * methods will be given a chance to execute. After all methods have
267         * executed, the first caught exception, if any, will be rethrown.
268         * @param testContext the current test context
269         */
270        protected void runAfterTransactionMethods(TestContext testContext) throws Exception {
271                Throwable afterTransactionException = null;
272
273                List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class);
274                for (Method method : methods) {
275                        try {
276                                if (logger.isDebugEnabled()) {
277                                        logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext);
278                                }
279                                ReflectionUtils.makeAccessible(method);
280                                method.invoke(testContext.getTestInstance());
281                        }
282                        catch (InvocationTargetException ex) {
283                                Throwable targetException = ex.getTargetException();
284                                if (afterTransactionException == null) {
285                                        afterTransactionException = targetException;
286                                }
287                                logger.error("Exception encountered while executing @AfterTransaction method [" + method +
288                                                "] for test context " + testContext, targetException);
289                        }
290                        catch (Exception ex) {
291                                if (afterTransactionException == null) {
292                                        afterTransactionException = ex;
293                                }
294                                logger.error("Exception encountered while executing @AfterTransaction method [" + method +
295                                                "] for test context " + testContext, ex);
296                        }
297                }
298
299                if (afterTransactionException != null) {
300                        ReflectionUtils.rethrowException(afterTransactionException);
301                }
302        }
303
304        /**
305         * Get the {@linkplain PlatformTransactionManager transaction manager} to use
306         * for the supplied {@linkplain TestContext test context} and {@code qualifier}.
307         * <p>Delegates to {@link #getTransactionManager(TestContext)} if the
308         * supplied {@code qualifier} is {@code null} or empty.
309         * @param testContext the test context for which the transaction manager
310         * should be retrieved
311         * @param qualifier the qualifier for selecting between multiple bean matches;
312         * may be {@code null} or empty
313         * @return the transaction manager to use, or {@code null} if not found
314         * @throws BeansException if an error occurs while retrieving the transaction manager
315         * @see #getTransactionManager(TestContext)
316         */
317        protected PlatformTransactionManager getTransactionManager(TestContext testContext, String qualifier) {
318                // Look up by type and qualifier from @Transactional
319                if (StringUtils.hasText(qualifier)) {
320                        try {
321                                // Use autowire-capable factory in order to support extended qualifier matching
322                                // (only exposed on the internal BeanFactory, not on the ApplicationContext).
323                                BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
324
325                                return BeanFactoryAnnotationUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier);
326                        }
327                        catch (RuntimeException ex) {
328                                if (logger.isWarnEnabled()) {
329                                        logger.warn(String.format(
330                                                        "Caught exception while retrieving transaction manager with qualifier '%s' for test context %s",
331                                                        qualifier, testContext), ex);
332                                }
333                                throw ex;
334                        }
335                }
336
337                // else
338                return getTransactionManager(testContext);
339        }
340
341        /**
342         * Get the {@linkplain PlatformTransactionManager transaction manager}
343         * to use for the supplied {@linkplain TestContext test context}.
344         * <p>The default implementation simply delegates to
345         * {@link TestContextTransactionUtils#retrieveTransactionManager}.
346         * @param testContext the test context for which the transaction manager
347         * should be retrieved
348         * @return the transaction manager to use, or {@code null} if not found
349         * @throws BeansException if an error occurs while retrieving an explicitly
350         * named transaction manager
351         * @throws IllegalStateException if more than one TransactionManagementConfigurer
352         * exists in the ApplicationContext
353         * @see #getTransactionManager(TestContext, String)
354         */
355        protected PlatformTransactionManager getTransactionManager(TestContext testContext) {
356                @SuppressWarnings("deprecation")
357                String tmName = retrieveConfigurationAttributes(testContext).getTransactionManagerName();
358                return TestContextTransactionUtils.retrieveTransactionManager(testContext, tmName);
359        }
360
361        /**
362         * Determine whether or not to rollback transactions by default for the
363         * supplied {@linkplain TestContext test context}.
364         * <p>Supports {@link Rollback @Rollback}, {@link Commit @Commit}, or
365         * {@link TransactionConfiguration @TransactionConfiguration} at the
366         * class-level.
367         * @param testContext the test context for which the default rollback flag
368         * should be retrieved
369         * @return the <em>default rollback</em> flag for the supplied test context
370         * @throws Exception if an error occurs while determining the default rollback flag
371         */
372        @SuppressWarnings("deprecation")
373        protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
374                Class<?> testClass = testContext.getTestClass();
375                Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class);
376                boolean rollbackPresent = (rollback != null);
377                TransactionConfigurationAttributes txConfigAttributes = retrieveConfigurationAttributes(testContext);
378
379                if (rollbackPresent && txConfigAttributes != defaultTxConfigAttributes) {
380                        throw new IllegalStateException(String.format("Test class [%s] is annotated with both @Rollback " +
381                                        "and @TransactionConfiguration, but only one is permitted.", testClass.getName()));
382                }
383
384                if (rollbackPresent) {
385                        boolean defaultRollback = rollback.value();
386                        if (logger.isDebugEnabled()) {
387                                logger.debug(String.format("Retrieved default @Rollback(%s) for test class [%s].",
388                                                defaultRollback, testClass.getName()));
389                        }
390                        return defaultRollback;
391                }
392
393                // else
394                return txConfigAttributes.isDefaultRollback();
395        }
396
397        /**
398         * Determine whether or not to rollback transactions for the supplied
399         * {@linkplain TestContext test context} by taking into consideration the
400         * {@linkplain #isDefaultRollback(TestContext) default rollback} flag and a
401         * possible method-level override via the {@link Rollback @Rollback}
402         * annotation.
403         * @param testContext the test context for which the rollback flag
404         * should be retrieved
405         * @return the <em>rollback</em> flag for the supplied test context
406         * @throws Exception if an error occurs while determining the rollback flag
407         */
408        protected final boolean isRollback(TestContext testContext) throws Exception {
409                boolean rollback = isDefaultRollback(testContext);
410                Rollback rollbackAnnotation =
411                                AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), Rollback.class);
412                if (rollbackAnnotation != null) {
413                        boolean rollbackOverride = rollbackAnnotation.value();
414                        if (logger.isDebugEnabled()) {
415                                logger.debug(String.format(
416                                                "Method-level @Rollback(%s) overrides default rollback [%s] for test context %s.",
417                                                rollbackOverride, rollback, testContext));
418                        }
419                        rollback = rollbackOverride;
420                }
421                else {
422                        if (logger.isDebugEnabled()) {
423                                logger.debug(String.format(
424                                                "No method-level @Rollback override: using default rollback [%s] for test context %s.",
425                                                rollback, testContext));
426                        }
427                }
428                return rollback;
429        }
430
431        /**
432         * Get all methods in the supplied {@link Class class} and its superclasses
433         * which are annotated with the supplied {@code annotationType} but
434         * which are not <em>shadowed</em> by methods overridden in subclasses.
435         * <p>Default methods on interfaces are also detected.
436         * @param clazz the class for which to retrieve the annotated methods
437         * @param annotationType the annotation type for which to search
438         * @return all annotated methods in the supplied class and its superclasses
439         * as well as annotated interface default methods
440         */
441        private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) {
442                List<Method> methods = new ArrayList<Method>(4);
443                for (Method method : ReflectionUtils.getUniqueDeclaredMethods(clazz)) {
444                        if (AnnotationUtils.getAnnotation(method, annotationType) != null) {
445                                methods.add(method);
446                        }
447                }
448                return methods;
449        }
450
451        /**
452         * Retrieve the {@link TransactionConfigurationAttributes} for the
453         * supplied {@link TestContext} whose {@linkplain Class test class}
454         * may optionally declare or inherit
455         * {@link TransactionConfiguration @TransactionConfiguration}.
456         * <p>If {@code @TransactionConfiguration} is not present for the
457         * supplied {@code TestContext}, a default instance of
458         * {@code TransactionConfigurationAttributes} will be used instead.
459         * @param testContext the test context for which the configuration
460         * attributes should be retrieved
461         * @return the TransactionConfigurationAttributes instance for this listener,
462         * potentially cached
463         * @see TransactionConfigurationAttributes#TransactionConfigurationAttributes()
464         */
465        @SuppressWarnings("deprecation")
466        TransactionConfigurationAttributes retrieveConfigurationAttributes(TestContext testContext) {
467                if (this.configurationAttributes == null) {
468                        Class<?> clazz = testContext.getTestClass();
469
470                        TransactionConfiguration txConfig =
471                                        AnnotatedElementUtils.findMergedAnnotation(clazz, TransactionConfiguration.class);
472                        if (logger.isDebugEnabled()) {
473                                logger.debug(String.format("Retrieved @TransactionConfiguration [%s] for test class [%s].",
474                                                txConfig, clazz.getName()));
475                        }
476
477                        TransactionConfigurationAttributes configAttributes = (txConfig == null ? defaultTxConfigAttributes :
478                                        new TransactionConfigurationAttributes(txConfig.transactionManager(), txConfig.defaultRollback()));
479                        if (logger.isDebugEnabled()) {
480                                logger.debug(String.format("Using TransactionConfigurationAttributes %s for test class [%s].",
481                                                configAttributes, clazz.getName()));
482                        }
483                        this.configurationAttributes = configAttributes;
484                }
485                return this.configurationAttributes;
486        }
487
488}