001/*
002 * Copyright 2002-2019 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.Arrays;
023import java.util.Collections;
024import java.util.List;
025import java.util.stream.Collectors;
026
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029
030import org.springframework.beans.BeansException;
031import org.springframework.beans.factory.BeanFactory;
032import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
033import org.springframework.core.annotation.AnnotatedElementUtils;
034import org.springframework.lang.Nullable;
035import org.springframework.test.annotation.Commit;
036import org.springframework.test.annotation.Rollback;
037import org.springframework.test.context.TestContext;
038import org.springframework.test.context.support.AbstractTestExecutionListener;
039import org.springframework.transaction.PlatformTransactionManager;
040import org.springframework.transaction.TransactionDefinition;
041import org.springframework.transaction.TransactionStatus;
042import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
043import org.springframework.transaction.annotation.Transactional;
044import org.springframework.transaction.interceptor.TransactionAttribute;
045import org.springframework.transaction.interceptor.TransactionAttributeSource;
046import org.springframework.util.Assert;
047import org.springframework.util.ReflectionUtils;
048import org.springframework.util.StringUtils;
049
050/**
051 * {@code TestExecutionListener} that provides support for executing tests
052 * within <em>test-managed transactions</em> by honoring Spring's
053 * {@link Transactional @Transactional} annotation.
054 *
055 * <h3>Test-managed Transactions</h3>
056 * <p><em>Test-managed transactions</em> are transactions that are managed
057 * declaratively via this listener or programmatically via
058 * {@link TestTransaction}. Such transactions should not be confused with
059 * <em>Spring-managed transactions</em> (i.e., those managed directly
060 * by Spring within the {@code ApplicationContext} loaded for tests) or
061 * <em>application-managed transactions</em> (i.e., those managed
062 * programmatically within application code that is invoked via tests).
063 * Spring-managed and application-managed transactions will typically
064 * participate in test-managed transactions; however, caution should be
065 * taken if Spring-managed or application-managed transactions are
066 * configured with any propagation type other than
067 * {@link org.springframework.transaction.annotation.Propagation#REQUIRED REQUIRED}
068 * or {@link org.springframework.transaction.annotation.Propagation#SUPPORTS SUPPORTS}.
069 *
070 * <h3>Enabling and Disabling Transactions</h3>
071 * <p>Annotating a test method with {@code @Transactional} causes the test
072 * to be run within a transaction that will, by default, be automatically
073 * <em>rolled back</em> after completion of the test. If a test class is
074 * annotated with {@code @Transactional}, each test method within that class
075 * hierarchy will be run within a transaction. Test methods that are
076 * <em>not</em> annotated with {@code @Transactional} (at the class or method
077 * level) will not be run within a transaction. Furthermore, tests that
078 * <em>are</em> annotated with {@code @Transactional} but have the
079 * {@link Transactional#propagation propagation} 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 Transactional @Transactional} (e.g., {@code @Transactional("myTxMgr")}
112 * or {@code @Transactional(transactionManager = "myTxMgr")}, or
113 * {@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 * <h3>{@code @Transactional} Attribute Support</h3>
121 * <table border="1">
122 * <tr><th>Attribute</th><th>Supported for test-managed transactions</th></tr>
123 * <tr><td>{@link Transactional#value value} and {@link Transactional#transactionManager transactionManager}</td><td>yes</td></tr>
124 * <tr><td>{@link Transactional#propagation propagation}</td>
125 * <td>only {@link org.springframework.transaction.annotation.Propagation#NOT_SUPPORTED NOT_SUPPORTED} is supported</td></tr>
126 * <tr><td>{@link Transactional#isolation isolation}</td><td>no</td></tr>
127 * <tr><td>{@link Transactional#timeout timeout}</td><td>no</td></tr>
128 * <tr><td>{@link Transactional#readOnly readOnly}</td><td>no</td></tr>
129 * <tr><td>{@link Transactional#rollbackFor rollbackFor} and {@link Transactional#rollbackForClassName rollbackForClassName}</td>
130 * <td>no: use {@link TestTransaction#flagForRollback()} instead</td></tr>
131 * <tr><td>{@link Transactional#noRollbackFor noRollbackFor} and {@link Transactional#noRollbackForClassName noRollbackForClassName}</td>
132 * <td>no: use {@link TestTransaction#flagForCommit()} instead</td></tr>
133 * </table>
134 *
135 * @author Sam Brannen
136 * @author Juergen Hoeller
137 * @since 2.5
138 * @see org.springframework.transaction.annotation.TransactionManagementConfigurer
139 * @see org.springframework.transaction.annotation.Transactional
140 * @see org.springframework.test.annotation.Commit
141 * @see org.springframework.test.annotation.Rollback
142 * @see BeforeTransaction
143 * @see AfterTransaction
144 * @see TestTransaction
145 */
146public class TransactionalTestExecutionListener extends AbstractTestExecutionListener {
147
148        private static final Log logger = LogFactory.getLog(TransactionalTestExecutionListener.class);
149
150        // Do not require @Transactional test methods to be public.
151        protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false);
152
153
154        /**
155         * Returns {@code 4000}.
156         */
157        @Override
158        public final int getOrder() {
159                return 4000;
160        }
161
162        /**
163         * If the test method of the supplied {@linkplain TestContext test context}
164         * is configured to run within a transaction, this method will run
165         * {@link BeforeTransaction @BeforeTransaction} methods and start a new
166         * transaction.
167         * <p>Note that if a {@code @BeforeTransaction} method fails, any remaining
168         * {@code @BeforeTransaction} methods will not be invoked, and a transaction
169         * will not be started.
170         * @see Transactional @Transactional
171         * @see #getTransactionManager(TestContext, String)
172         */
173        @Override
174        public void beforeTestMethod(final TestContext testContext) throws Exception {
175                Method testMethod = testContext.getTestMethod();
176                Class<?> testClass = testContext.getTestClass();
177                Assert.notNull(testMethod, "Test method of supplied TestContext must not be null");
178
179                TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
180                Assert.state(txContext == null, "Cannot start new transaction without ending existing transaction");
181
182                PlatformTransactionManager tm = null;
183                TransactionAttribute transactionAttribute = this.attributeSource.getTransactionAttribute(testMethod, testClass);
184
185                if (transactionAttribute != null) {
186                        transactionAttribute = TestContextTransactionUtils.createDelegatingTransactionAttribute(testContext,
187                                transactionAttribute);
188
189                        if (logger.isDebugEnabled()) {
190                                logger.debug("Explicit transaction definition [" + transactionAttribute +
191                                                "] found for test context " + testContext);
192                        }
193
194                        if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
195                                return;
196                        }
197
198                        tm = getTransactionManager(testContext, transactionAttribute.getQualifier());
199                        Assert.state(tm != null,
200                                        () -> "Failed to retrieve PlatformTransactionManager for @Transactional test: " + testContext);
201                }
202
203                if (tm != null) {
204                        txContext = new TransactionContext(testContext, tm, transactionAttribute, isRollback(testContext));
205                        runBeforeTransactionMethods(testContext);
206                        txContext.startTransaction();
207                        TransactionContextHolder.setCurrentTransactionContext(txContext);
208                }
209        }
210
211        /**
212         * If a transaction is currently active for the supplied
213         * {@linkplain TestContext test context}, this method will end the transaction
214         * and run {@link AfterTransaction @AfterTransaction} methods.
215         * <p>{@code @AfterTransaction} methods are guaranteed to be invoked even if
216         * an error occurs while ending the transaction.
217         */
218        @Override
219        public void afterTestMethod(TestContext testContext) throws Exception {
220                Method testMethod = testContext.getTestMethod();
221                Assert.notNull(testMethod, "The test method of the supplied TestContext must not be null");
222
223                TransactionContext txContext = TransactionContextHolder.removeCurrentTransactionContext();
224                // If there was (or perhaps still is) a transaction...
225                if (txContext != null) {
226                        TransactionStatus transactionStatus = txContext.getTransactionStatus();
227                        try {
228                                // If the transaction is still active...
229                                if (transactionStatus != null && !transactionStatus.isCompleted()) {
230                                        txContext.endTransaction();
231                                }
232                        }
233                        finally {
234                                runAfterTransactionMethods(testContext);
235                        }
236                }
237        }
238
239        /**
240         * Run all {@link BeforeTransaction @BeforeTransaction} methods for the
241         * specified {@linkplain TestContext test context}. If one of the methods
242         * fails, however, the caught exception will be rethrown in a wrapped
243         * {@link RuntimeException}, and the remaining methods will <strong>not</strong>
244         * be given a chance to execute.
245         * @param testContext the current test context
246         */
247        protected void runBeforeTransactionMethods(TestContext testContext) throws Exception {
248                try {
249                        List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class);
250                        Collections.reverse(methods);
251                        for (Method method : methods) {
252                                if (logger.isDebugEnabled()) {
253                                        logger.debug("Executing @BeforeTransaction method [" + method + "] for test context " + testContext);
254                                }
255                                ReflectionUtils.makeAccessible(method);
256                                method.invoke(testContext.getTestInstance());
257                        }
258                }
259                catch (InvocationTargetException ex) {
260                        if (logger.isErrorEnabled()) {
261                                logger.error("Exception encountered while executing @BeforeTransaction methods for test context " +
262                                                testContext + ".", ex.getTargetException());
263                        }
264                        ReflectionUtils.rethrowException(ex.getTargetException());
265                }
266        }
267
268        /**
269         * Run all {@link AfterTransaction @AfterTransaction} methods for the
270         * specified {@linkplain TestContext test context}. If one of the methods
271         * fails, the caught exception will be logged as an error, and the remaining
272         * methods will be given a chance to execute. After all methods have
273         * executed, the first caught exception, if any, will be rethrown.
274         * @param testContext the current test context
275         */
276        protected void runAfterTransactionMethods(TestContext testContext) throws Exception {
277                Throwable afterTransactionException = null;
278
279                List<Method> methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class);
280                for (Method method : methods) {
281                        try {
282                                if (logger.isDebugEnabled()) {
283                                        logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext);
284                                }
285                                ReflectionUtils.makeAccessible(method);
286                                method.invoke(testContext.getTestInstance());
287                        }
288                        catch (InvocationTargetException ex) {
289                                Throwable targetException = ex.getTargetException();
290                                if (afterTransactionException == null) {
291                                        afterTransactionException = targetException;
292                                }
293                                logger.error("Exception encountered while executing @AfterTransaction method [" + method +
294                                                "] for test context " + testContext, targetException);
295                        }
296                        catch (Exception ex) {
297                                if (afterTransactionException == null) {
298                                        afterTransactionException = ex;
299                                }
300                                logger.error("Exception encountered while executing @AfterTransaction method [" + method +
301                                                "] for test context " + testContext, ex);
302                        }
303                }
304
305                if (afterTransactionException != null) {
306                        ReflectionUtils.rethrowException(afterTransactionException);
307                }
308        }
309
310        /**
311         * Get the {@linkplain PlatformTransactionManager transaction manager} to use
312         * for the supplied {@linkplain TestContext test context} and {@code qualifier}.
313         * <p>Delegates to {@link #getTransactionManager(TestContext)} if the
314         * supplied {@code qualifier} is {@code null} or empty.
315         * @param testContext the test context for which the transaction manager
316         * should be retrieved
317         * @param qualifier the qualifier for selecting between multiple bean matches;
318         * may be {@code null} or empty
319         * @return the transaction manager to use, or {@code null} if not found
320         * @throws BeansException if an error occurs while retrieving the transaction manager
321         * @see #getTransactionManager(TestContext)
322         */
323        @Nullable
324        protected PlatformTransactionManager getTransactionManager(TestContext testContext, @Nullable String qualifier) {
325                // Look up by type and qualifier from @Transactional
326                if (StringUtils.hasText(qualifier)) {
327                        try {
328                                // Use autowire-capable factory in order to support extended qualifier matching
329                                // (only exposed on the internal BeanFactory, not on the ApplicationContext).
330                                BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory();
331
332                                return BeanFactoryAnnotationUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier);
333                        }
334                        catch (RuntimeException ex) {
335                                if (logger.isWarnEnabled()) {
336                                        logger.warn(String.format(
337                                                        "Caught exception while retrieving transaction manager with qualifier '%s' for test context %s",
338                                                        qualifier, testContext), ex);
339                                }
340                                throw ex;
341                        }
342                }
343
344                // else
345                return getTransactionManager(testContext);
346        }
347
348        /**
349         * Get the {@linkplain PlatformTransactionManager transaction manager}
350         * to use for the supplied {@linkplain TestContext test context}.
351         * <p>The default implementation simply delegates to
352         * {@link TestContextTransactionUtils#retrieveTransactionManager}.
353         * @param testContext the test context for which the transaction manager
354         * should be retrieved
355         * @return the transaction manager to use, or {@code null} if not found
356         * @throws BeansException if an error occurs while retrieving an explicitly
357         * named transaction manager
358         * @throws IllegalStateException if more than one TransactionManagementConfigurer
359         * exists in the ApplicationContext
360         * @see #getTransactionManager(TestContext, String)
361         */
362        @Nullable
363        protected PlatformTransactionManager getTransactionManager(TestContext testContext) {
364                return TestContextTransactionUtils.retrieveTransactionManager(testContext, null);
365        }
366
367        /**
368         * Determine whether or not to rollback transactions by default for the
369         * supplied {@linkplain TestContext test context}.
370         * <p>Supports {@link Rollback @Rollback} or {@link Commit @Commit} at the
371         * class-level.
372         * @param testContext the test context for which the default rollback flag
373         * should be retrieved
374         * @return the <em>default rollback</em> flag for the supplied test context
375         * @throws Exception if an error occurs while determining the default rollback flag
376         */
377        protected final boolean isDefaultRollback(TestContext testContext) throws Exception {
378                Class<?> testClass = testContext.getTestClass();
379                Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class);
380                boolean rollbackPresent = (rollback != null);
381
382                if (rollbackPresent) {
383                        boolean defaultRollback = rollback.value();
384                        if (logger.isDebugEnabled()) {
385                                logger.debug(String.format("Retrieved default @Rollback(%s) for test class [%s].",
386                                                defaultRollback, testClass.getName()));
387                        }
388                        return defaultRollback;
389                }
390
391                // else
392                return true;
393        }
394
395        /**
396         * Determine whether or not to rollback transactions for the supplied
397         * {@linkplain TestContext test context} by taking into consideration the
398         * {@linkplain #isDefaultRollback(TestContext) default rollback} flag and a
399         * possible method-level override via the {@link Rollback @Rollback}
400         * annotation.
401         * @param testContext the test context for which the rollback flag
402         * should be retrieved
403         * @return the <em>rollback</em> flag for the supplied test context
404         * @throws Exception if an error occurs while determining the rollback flag
405         */
406        protected final boolean isRollback(TestContext testContext) throws Exception {
407                boolean rollback = isDefaultRollback(testContext);
408                Rollback rollbackAnnotation =
409                                AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), Rollback.class);
410                if (rollbackAnnotation != null) {
411                        boolean rollbackOverride = rollbackAnnotation.value();
412                        if (logger.isDebugEnabled()) {
413                                logger.debug(String.format(
414                                                "Method-level @Rollback(%s) overrides default rollback [%s] for test context %s.",
415                                                rollbackOverride, rollback, testContext));
416                        }
417                        rollback = rollbackOverride;
418                }
419                else {
420                        if (logger.isDebugEnabled()) {
421                                logger.debug(String.format(
422                                                "No method-level @Rollback override: using default rollback [%s] for test context %s.",
423                                                rollback, testContext));
424                        }
425                }
426                return rollback;
427        }
428
429        /**
430         * Get all methods in the supplied {@link Class class} and its superclasses
431         * which are annotated with the supplied {@code annotationType} but
432         * which are not <em>shadowed</em> by methods overridden in subclasses.
433         * <p>Default methods on interfaces are also detected.
434         * @param clazz the class for which to retrieve the annotated methods
435         * @param annotationType the annotation type for which to search
436         * @return all annotated methods in the supplied class and its superclasses
437         * as well as annotated interface default methods
438         */
439        private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) {
440                return Arrays.stream(ReflectionUtils.getUniqueDeclaredMethods(clazz, ReflectionUtils.USER_DECLARED_METHODS))
441                                .filter(method -> AnnotatedElementUtils.hasAnnotation(method, annotationType))
442                                .collect(Collectors.toList());
443        }
444
445}