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}