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}