/* * Copyright 2002-2008 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.test.annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Map; import javax.sql.DataSource; import junit.framework.AssertionFailedError; import org.springframework.context.ApplicationContext; import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; import org.springframework.transaction.interceptor.TransactionAttributeSource; import org.springframework.util.Assert; /** * <p> * Java 5 specific subclass of * {@link AbstractTransactionalDataSourceSpringContextTests}, exposing a * {@link SimpleJdbcTemplate} and obeying annotations for transaction control. * </p> * <p> * For example, test methods can be annotated with the regular Spring * {@link org.springframework.transaction.annotation.Transactional @Transactional} * annotation (e.g., to force execution in a read-only transaction) or with the * {@link NotTransactional @NotTransactional} annotation to prevent any * transaction being created at all. In addition, individual test methods can be * annotated with {@link Rollback @Rollback} to override the * {@link #isDefaultRollback() default rollback} settings. * </p> * <p> * The following list constitutes all annotations currently supported by * AbstractAnnotationAwareTransactionalTests: * </p> * <ul> * <li>{@link DirtiesContext @DirtiesContext}</li> * <li>{@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}</li> * <li>{@link IfProfileValue @IfProfileValue}</li> * <li>{@link ExpectedException @ExpectedException}</li> * <li>{@link Timed @Timed}</li> * <li>{@link Repeat @Repeat}</li> * <li>{@link org.springframework.transaction.annotation.Transactional @Transactional}</li> * <li>{@link NotTransactional @NotTransactional}</li> * <li>{@link Rollback @Rollback}</li> * </ul> * * @author Rod Johnson * @author Sam Brannen * @author Juergen Hoeller * @since 2.0 */ public abstract class AbstractAnnotationAwareTransactionalTests extends AbstractTransactionalDataSourceSpringContextTests { protected SimpleJdbcTemplate simpleJdbcTemplate; private final TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource(); /** * {@link ProfileValueSource} available to subclasses but primarily intended * for use in {@link #isDisabledInThisEnvironment(Method)}. * <p>Set to {@link SystemProfileValueSource} by default for backwards * compatibility; however, the value may be changed in the * {@link #AbstractAnnotationAwareTransactionalTests(String)} constructor. */ protected ProfileValueSource profileValueSource = SystemProfileValueSource.getInstance(); /** * Default constructor for AbstractAnnotationAwareTransactionalTests, which * delegates to {@link #AbstractAnnotationAwareTransactionalTests(String)}. */ public AbstractAnnotationAwareTransactionalTests() { this(null); } /** * Constructs a new AbstractAnnotationAwareTransactionalTests instance with * the specified JUnit <code>name</code> and retrieves the configured (or * default) {@link ProfileValueSource}. * @param name the name of the current test * @see ProfileValueUtils#retrieveProfileValueSource(Class) */ public AbstractAnnotationAwareTransactionalTests(String name) { super(name); this.profileValueSource = ProfileValueUtils.retrieveProfileValueSource(getClass()); } @Override public void setDataSource(DataSource dataSource) { super.setDataSource(dataSource); // JdbcTemplate will be identically configured this.simpleJdbcTemplate = new SimpleJdbcTemplate(this.jdbcTemplate); } /** * Search for a unique {@link ProfileValueSource} in the supplied * {@link ApplicationContext}. If found, the * <code>profileValueSource</code> for this test will be set to the unique * {@link ProfileValueSource}. * @param applicationContext the ApplicationContext in which to search for * the ProfileValueSource; may not be <code>null</code> * @deprecated Use {@link ProfileValueSourceConfiguration @ProfileValueSourceConfiguration} instead. */ @Deprecated protected void findUniqueProfileValueSourceFromContext(ApplicationContext applicationContext) { Assert.notNull(applicationContext, "Can not search for a ProfileValueSource in a null ApplicationContext."); ProfileValueSource uniqueProfileValueSource = null; Map<?, ?> beans = applicationContext.getBeansOfType(ProfileValueSource.class); if (beans.size() == 1) { uniqueProfileValueSource = (ProfileValueSource) beans.values().iterator().next(); } if (uniqueProfileValueSource != null) { this.profileValueSource = uniqueProfileValueSource; } } /** * Overridden to populate transaction definition from annotations. */ @Override public void runBare() throws Throwable { // getName will return the name of the method being run. if (isDisabledInThisEnvironment(getName())) { // Let superclass log that we didn't run the test. super.runBare(); return; } final Method testMethod = getTestMethod(); if (isDisabledInThisEnvironment(testMethod)) { recordDisabled(); this.logger.info("**** " + getClass().getName() + "." + getName() + " disabled in this environment: " + "Total disabled tests=" + getDisabledTestCount()); return; } TransactionDefinition explicitTransactionDefinition = this.transactionAttributeSource.getTransactionAttribute(testMethod, getClass()); if (explicitTransactionDefinition != null) { this.logger.info("Custom transaction definition [" + explicitTransactionDefinition + "] for test method [" + getName() + "]."); setTransactionDefinition(explicitTransactionDefinition); } else if (testMethod.isAnnotationPresent(NotTransactional.class)) { // Don't have any transaction... preventTransaction(); } // Let JUnit handle execution. We're just changing the state of the test class first. runTestTimed(new TestExecutionCallback() { public void run() throws Throwable { try { AbstractAnnotationAwareTransactionalTests.super.runBare(); } finally { // Mark the context to be blown away if the test was // annotated to result in setDirty being invoked // automatically. if (testMethod.isAnnotationPresent(DirtiesContext.class)) { AbstractAnnotationAwareTransactionalTests.this.setDirty(); } } } }, testMethod); } /** * Determine if the test for the supplied <code>testMethod</code> should * run in the current environment. * <p>The default implementation is based on * {@link IfProfileValue @IfProfileValue} semantics. * @param testMethod the test method * @return <code>true</code> if the test is <em>disabled</em> in the current environment * @see ProfileValueUtils#isTestEnabledInThisEnvironment */ protected boolean isDisabledInThisEnvironment(Method testMethod) { return !ProfileValueUtils.isTestEnabledInThisEnvironment(this.profileValueSource, testMethod, getClass()); } /** * Get the current test method. */ protected Method getTestMethod() { assertNotNull("TestCase.getName() cannot be null", getName()); Method testMethod = null; try { // Use same algorithm as JUnit itself to retrieve the test method // about to be executed (the method name is returned by getName). It // has to be public so we can retrieve it. testMethod = getClass().getMethod(getName(), (Class[]) null); } catch (NoSuchMethodException ex) { fail("Method '" + getName() + "' not found"); } if (!Modifier.isPublic(testMethod.getModifiers())) { fail("Method '" + getName() + "' should be public"); } return testMethod; } /** * Determine whether or not to rollback transactions for the current test * by taking into consideration the * {@link #isDefaultRollback() default rollback} flag and a possible * method-level override via the {@link Rollback @Rollback} annotation. * @return the <em>rollback</em> flag for the current test */ @Override protected boolean isRollback() { boolean rollback = isDefaultRollback(); Rollback rollbackAnnotation = getTestMethod().getAnnotation(Rollback.class); if (rollbackAnnotation != null) { boolean rollbackOverride = rollbackAnnotation.value(); if (this.logger.isDebugEnabled()) { this.logger.debug("Method-level @Rollback(" + rollbackOverride + ") overrides default rollback [" + rollback + "] for test [" + getName() + "]."); } rollback = rollbackOverride; } else { if (this.logger.isDebugEnabled()) { this.logger.debug("No method-level @Rollback override: using default rollback [" + rollback + "] for test [" + getName() + "]."); } } return rollback; } private void runTestTimed(TestExecutionCallback tec, Method testMethod) throws Throwable { Timed timed = testMethod.getAnnotation(Timed.class); if (timed == null) { runTest(tec, testMethod); } else { long startTime = System.currentTimeMillis(); try { runTest(tec, testMethod); } finally { long elapsed = System.currentTimeMillis() - startTime; if (elapsed > timed.millis()) { fail("Took " + elapsed + " ms; limit was " + timed.millis()); } } } } private void runTest(TestExecutionCallback tec, Method testMethod) throws Throwable { ExpectedException expectedExceptionAnnotation = testMethod.getAnnotation(ExpectedException.class); boolean exceptionIsExpected = (expectedExceptionAnnotation != null && expectedExceptionAnnotation.value() != null); Class<? extends Throwable> expectedException = (exceptionIsExpected ? expectedExceptionAnnotation.value() : null); Repeat repeat = testMethod.getAnnotation(Repeat.class); int runs = ((repeat != null) && (repeat.value() > 1)) ? repeat.value() : 1; for (int i = 0; i < runs; i++) { try { if (runs > 1 && this.logger != null && this.logger.isInfoEnabled()) { this.logger.info("Repetition " + (i + 1) + " of test " + testMethod.getName()); } tec.run(); if (exceptionIsExpected) { fail("Expected exception: " + expectedException.getName()); } } catch (Throwable t) { if (!exceptionIsExpected) { throw t; } if (!expectedException.isAssignableFrom(t.getClass())) { // Wrap the unexpected throwable with an explicit message. AssertionFailedError assertionError = new AssertionFailedError("Unexpected exception, expected<" + expectedException.getName() + "> but was<" + t.getClass().getName() + ">"); assertionError.initCause(t); throw assertionError; } } } } private static interface TestExecutionCallback { void run() throws Throwable; } }