diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 446bd58dfd..a2413b1b39 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -40,23 +40,29 @@ import org.springframework.util.ReflectionUtils; *
  • {@link #beforeTestClass() before test class execution}: prior to any * before class callbacks of a particular testing framework (e.g., * JUnit 4's {@link org.junit.BeforeClass @BeforeClass})
  • - *
  • {@link #prepareTestInstance(Object) test instance preparation}: - * immediately following instantiation of the test instance
  • - *
  • {@link #beforeTestMethod(Object, Method) before test method execution}: + *
  • {@link #prepareTestInstance test instance preparation}: + * immediately following instantiation of the test class
  • + *
  • {@link #beforeTestMethod before test setup}: * prior to any before method callbacks of a particular testing framework * (e.g., JUnit 4's {@link org.junit.Before @Before})
  • - *
  • {@link #afterTestMethod(Object, Method, Throwable) after test method - * execution}: after any after method callbacks of a particular testing + *
  • {@link #beforeTestExecution before test execution}: + * immediately before execution of the {@linkplain java.lang.reflect.Method + * test method} but after test setup
  • + *
  • {@link #afterTestExecution after test execution}: + * immediately after execution of the {@linkplain java.lang.reflect.Method + * test method} but before test tear down
  • + *
  • {@link #afterTestMethod(Object, Method, Throwable) after test tear down}: + * after any after method callbacks of a particular testing * framework (e.g., JUnit 4's {@link org.junit.After @After})
  • *
  • {@link #afterTestClass() after test class execution}: after any - * after class callbacks of a particular testing framework (e.g., JUnit - * 4's {@link org.junit.AfterClass @AfterClass})
  • + * after class callbacks of a particular testing framework (e.g., JUnit 4's + * {@link org.junit.AfterClass @AfterClass}) * * *

    Support for loading and accessing - * {@link org.springframework.context.ApplicationContext application contexts}, + * {@linkplain org.springframework.context.ApplicationContext application contexts}, * dependency injection of test instances, - * {@link org.springframework.transaction.annotation.Transactional transactional} + * {@linkplain org.springframework.transaction.annotation.Transactional transactional} * execution of test methods, etc. is provided by * {@link SmartContextLoader ContextLoaders} and {@link TestExecutionListener * TestExecutionListeners}, which are configured via @@ -173,7 +179,7 @@ public class TestContextManager { /** * Hook for pre-processing a test class before execution of any * tests within the class. Should be called prior to any framework-specific - * before class methods (e.g., methods annotated with JUnit's + * before class methods (e.g., methods annotated with JUnit 4's * {@link org.junit.BeforeClass @BeforeClass}). *

    An attempt will be made to give each registered * {@link TestExecutionListener} a chance to pre-process the test class @@ -181,6 +187,7 @@ public class TestContextManager { * registered listeners will not be called. * @throws Exception if a registered TestExecutionListener throws an * exception + * @since 3.0 * @see #getTestExecutionListeners() */ public void beforeTestClass() throws Exception { @@ -195,10 +202,7 @@ public class TestContextManager { testExecutionListener.beforeTestClass(getTestContext()); } catch (Throwable ex) { - if (logger.isWarnEnabled()) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'before class' callback for test class [" + testClass + "]", ex); - } + logException(ex, "beforeTestClass", testExecutionListener, testClass); ReflectionUtils.rethrowException(ex); } } @@ -240,60 +244,101 @@ public class TestContextManager { } /** - * Hook for pre-processing a test before execution of the supplied - * {@link Method test method}, for example for setting up test fixtures, - * starting a transaction, etc. Should be called prior to any - * framework-specific before methods (e.g., methods annotated with - * JUnit's {@link org.junit.Before @Before}). + * Hook for pre-processing a test before execution of before + * lifecycle callbacks of the underlying test framework — for example, + * setting up test fixtures, starting a transaction, etc. + *

    This method must be called immediately prior to + * framework-specific before lifecycle callbacks (e.g., methods + * annotated with JUnit 4's {@link org.junit.Before @Before}). For historical + * reasons, this method is named {@code beforeTestMethod}. Since the + * introduction of {@link #beforeTestExecution}, a more suitable name for + * this method might be something like {@code beforeTestSetUp} or + * {@code beforeEach}; however, it is unfortunately impossible to rename + * this method due to backward compatibility concerns. *

    The managed {@link TestContext} will be updated with the supplied * {@code testInstance} and {@code testMethod}. *

    An attempt will be made to give each registered - * {@link TestExecutionListener} a chance to pre-process the test method - * execution. If a listener throws an exception, however, the remaining - * registered listeners will not be called. + * {@link TestExecutionListener} a chance to perform its pre-processing. + * If a listener throws an exception, however, the remaining registered + * listeners will not be called. * @param testInstance the current test instance (never {@code null}) * @param testMethod the test method which is about to be executed on the * test instance * @throws Exception if a registered TestExecutionListener throws an exception + * @see #afterTestMethod + * @see #beforeTestExecution + * @see #afterTestExecution * @see #getTestExecutionListeners() */ public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception { - Assert.notNull(testInstance, "Test instance must not be null"); - if (logger.isTraceEnabled()) { - logger.trace("beforeTestMethod(): instance [" + testInstance + "], method [" + testMethod + "]"); - } - getTestContext().updateState(testInstance, testMethod, null); + String callbackName = "beforeTestMethod"; + prepareForBeforeCallback(callbackName, testInstance, testMethod); for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { try { testExecutionListener.beforeTestMethod(getTestContext()); } catch (Throwable ex) { - if (logger.isWarnEnabled()) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'before' execution of test method [" + testMethod + "] for test instance [" + - testInstance + "]", ex); - } - ReflectionUtils.rethrowException(ex); + handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod); + } + } + } + + /** + * Hook for pre-processing a test immediately before execution of + * the {@linkplain java.lang.reflect.Method test method} in the supplied + * {@linkplain TestContext test context} — for example, for timing + * or logging purposes. + *

    This method must be called after framework-specific + * before lifecycle callbacks (e.g., methods annotated with JUnit 4's + * {@link org.junit.Before @Before}). + *

    The managed {@link TestContext} will be updated with the supplied + * {@code testInstance} and {@code testMethod}. + *

    An attempt will be made to give each registered + * {@link TestExecutionListener} a chance to perform its pre-processing. + * If a listener throws an exception, however, the remaining registered + * listeners will not be called. + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which is about to be executed on the + * test instance + * @throws Exception if a registered TestExecutionListener throws an exception + * @since 5.0 + * @see #beforeTestMethod + * @see #afterTestMethod + * @see #beforeTestExecution + * @see #afterTestExecution + * @see #getTestExecutionListeners() + */ + public void beforeTestExecution(Object testInstance, Method testMethod) throws Exception { + String callbackName = "beforeTestExecution"; + prepareForBeforeCallback(callbackName, testInstance, testMethod); + + for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) { + try { + testExecutionListener.beforeTestExecution(getTestContext()); + } + catch (Throwable ex) { + handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod); } } } /** - * Hook for post-processing a test after execution of the supplied - * {@link Method test method}, for example for tearing down test fixtures, - * ending a transaction, etc. Should be called after any framework-specific - * after methods (e.g., methods annotated with JUnit's + * Hook for post-processing a test immediately after execution of + * the {@linkplain java.lang.reflect.Method test method} in the supplied + * {@linkplain TestContext test context} — for example, for timing + * or logging purposes. + *

    This method must be called before framework-specific + * after lifecycle callbacks (e.g., methods annotated with JUnit 4's * {@link org.junit.After @After}). *

    The managed {@link TestContext} will be updated with the supplied - * {@code testInstance}, {@code testMethod}, and - * {@code exception}. - *

    Each registered {@link TestExecutionListener} will be given a chance to - * post-process the test method execution. If a listener throws an - * exception, the remaining registered listeners will still be called, but - * the first exception thrown will be tracked and rethrown after all - * listeners have executed. Note that registered listeners will be executed - * in the opposite order in which they were registered. + * {@code testInstance}, {@code testMethod}, and {@code exception}. + *

    Each registered {@link TestExecutionListener} will be given a chance + * to perform its post-processing. If a listener throws an exception, the + * remaining registered listeners will still be called, but the first + * exception thrown will be tracked and rethrown after all listeners have + * executed. Note that registered listeners will be executed in the opposite + * order in which they were registered. * @param testInstance the current test instance (never {@code null}) * @param testMethod the test method which has just been executed on the * test instance @@ -301,15 +346,70 @@ public class TestContextManager { * test method or by a TestExecutionListener, or {@code null} if none * was thrown * @throws Exception if a registered TestExecutionListener throws an exception + * @since 5.0 + * @see #beforeTestMethod + * @see #afterTestMethod + * @see #beforeTestExecution * @see #getTestExecutionListeners() */ - public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception { - Assert.notNull(testInstance, "Test instance must not be null"); - if (logger.isTraceEnabled()) { - logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod + - "], exception [" + exception + "]"); + public void afterTestExecution(Object testInstance, Method testMethod, Throwable exception) throws Exception { + String callbackName = "afterTestExecution"; + prepareForAfterCallback(callbackName, testInstance, testMethod, exception); + + Throwable afterTestExecutionException = null; + // Traverse the TestExecutionListeners in reverse order to ensure proper + // "wrapper"-style execution of listeners. + for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { + try { + testExecutionListener.afterTestExecution(getTestContext()); + } + catch (Throwable ex) { + logException(ex, callbackName, testExecutionListener, testInstance, testMethod); + if (afterTestExecutionException == null) { + afterTestExecutionException = ex; + } + } } - getTestContext().updateState(testInstance, testMethod, exception); + if (afterTestExecutionException != null) { + ReflectionUtils.rethrowException(afterTestExecutionException); + } + } + + /** + * Hook for post-processing a test after execution of after + * lifecycle callbacks of the underlying test framework — for example, + * tearing down test fixtures, ending a transaction, etc. + *

    This method must be called immediately after + * framework-specific after lifecycle callbacks (e.g., methods + * annotated with JUnit 4's {@link org.junit.After @After}). For historical + * reasons, this method is named {@code afterTestMethod}. Since the + * introduction of {@link #afterTestExecution}, a more suitable name for + * this method might be something like {@code afterTestTearDown} or + * {@code afterEach}; however, it is unfortunately impossible to rename + * this method due to backward compatibility concerns. + *

    The managed {@link TestContext} will be updated with the supplied + * {@code testInstance}, {@code testMethod}, and {@code exception}. + *

    Each registered {@link TestExecutionListener} will be given a chance + * to perform its post-processing. If a listener throws an exception, the + * remaining registered listeners will still be called, but the first + * exception thrown will be tracked and rethrown after all listeners have + * executed. Note that registered listeners will be executed in the opposite + * order in which they were registered. + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which has just been executed on the + * test instance + * @param exception the exception that was thrown during execution of the + * test method or by a TestExecutionListener, or {@code null} if none + * was thrown + * @throws Exception if a registered TestExecutionListener throws an exception + * @see #beforeTestMethod + * @see #beforeTestExecution + * @see #afterTestExecution + * @see #getTestExecutionListeners() + */ + public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception { + String callbackName = "afterTestMethod"; + prepareForAfterCallback(callbackName, testInstance, testMethod, exception); Throwable afterTestMethodException = null; // Traverse the TestExecutionListeners in reverse order to ensure proper @@ -319,11 +419,7 @@ public class TestContextManager { testExecutionListener.afterTestMethod(getTestContext()); } catch (Throwable ex) { - if (logger.isWarnEnabled()) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'after' execution for test: method [" + testMethod + "], instance [" + - testInstance + "], exception [" + exception + "]", ex); - } + logException(ex, callbackName, testExecutionListener, testInstance, testMethod); if (afterTestMethodException == null) { afterTestMethodException = ex; } @@ -337,7 +433,7 @@ public class TestContextManager { /** * Hook for post-processing a test class after execution of all * tests within the class. Should be called after any framework-specific - * after class methods (e.g., methods annotated with JUnit's + * after class methods (e.g., methods annotated with JUnit 4's * {@link org.junit.AfterClass @AfterClass}). *

    Each registered {@link TestExecutionListener} will be given a chance to * post-process the test class. If a listener throws an exception, the @@ -346,6 +442,7 @@ public class TestContextManager { * executed. Note that registered listeners will be executed in the opposite * order in which they were registered. * @throws Exception if a registered TestExecutionListener throws an exception + * @since 3.0 * @see #getTestExecutionListeners() */ public void afterTestClass() throws Exception { @@ -363,10 +460,7 @@ public class TestContextManager { testExecutionListener.afterTestClass(getTestContext()); } catch (Throwable ex) { - if (logger.isWarnEnabled()) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'after class' callback for test class [" + testClass + "]", ex); - } + logException(ex, "afterTestClass", testExecutionListener, testClass); if (afterTestClassException == null) { afterTestClassException = ex; } @@ -377,4 +471,46 @@ public class TestContextManager { } } + private void prepareForBeforeCallback(String callbackName, Object testInstance, Method testMethod) { + Assert.notNull(testInstance, "Test instance must not be null"); + if (logger.isTraceEnabled()) { + logger.trace(String.format("%s(): instance [%s], method [%s]", callbackName, testInstance, testMethod)); + } + getTestContext().updateState(testInstance, testMethod, null); + } + + private void prepareForAfterCallback(String callbackName, Object testInstance, Method testMethod, + Throwable exception) { + Assert.notNull(testInstance, "Test instance must not be null"); + if (logger.isTraceEnabled()) { + logger.trace(String.format("%s(): instance [%s], method [%s], exception [%s]", callbackName, testInstance, + testMethod, exception)); + } + getTestContext().updateState(testInstance, testMethod, exception); + } + + private void handleBeforeException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener, + Object testInstance, Method testMethod) throws Exception { + logException(ex, callbackName, testExecutionListener, testInstance, testMethod); + ReflectionUtils.rethrowException(ex); + } + + private void logException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener, + Class testClass) { + if (logger.isWarnEnabled()) { + logger.warn(String.format("Caught exception while invoking '%s' callback on " + + "TestExecutionListener [%s] for test class [%s]", callbackName, testExecutionListener, + testClass), ex); + } + } + + private void logException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener, + Object testInstance, Method testMethod) { + if (logger.isWarnEnabled()) { + logger.warn(String.format("Caught exception while invoking '%s' callback on " + + "TestExecutionListener [%s] for test method [%s] and test instance [%s]", + callbackName, testExecutionListener, testMethod, testInstance), ex); + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/context/TestContextManagerTests.java b/spring-test/src/test/java/org/springframework/test/context/TestContextManagerTests.java index 3070ba4900..fb88f398a6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestContextManagerTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextManagerTests.java @@ -21,17 +21,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; -import org.springframework.core.style.ToStringCreator; -import org.springframework.test.context.support.AbstractTestExecutionListener; - import static org.junit.Assert.*; /** @@ -44,94 +35,77 @@ import static org.junit.Assert.*; */ public class TestContextManagerTests { - private static final String FIRST = "veni"; - private static final String SECOND = "vidi"; - private static final String THIRD = "vici"; - - private static final List afterTestMethodCalls = new ArrayList<>(); - private static final List beforeTestMethodCalls = new ArrayList<>(); - - protected static final Log logger = LogFactory.getLog(TestContextManagerTests.class); + private static final List executionOrder = new ArrayList<>(); private final TestContextManager testContextManager = new TestContextManager(ExampleTestCase.class); - - private Method getTestMethod() throws NoSuchMethodException { - return ExampleTestCase.class.getDeclaredMethod("exampleTestMethod", (Class[]) null); - } - - /** - * Asserts the execution order of 'before' and 'after' test method - * calls on {@link TestExecutionListener listeners} registered for the - * configured {@link TestContextManager}. - * - * @see #beforeTestMethodCalls - * @see #afterTestMethodCalls - */ - private static void assertExecutionOrder(List expectedBeforeTestMethodCalls, - List expectedAfterTestMethodCalls, final String usageContext) { - - if (expectedBeforeTestMethodCalls == null) { - expectedBeforeTestMethodCalls = new ArrayList<>(); - } - if (expectedAfterTestMethodCalls == null) { - expectedAfterTestMethodCalls = new ArrayList<>(); + private final Method testMethod; + { + try { + this.testMethod = ExampleTestCase.class.getDeclaredMethod("exampleTestMethod"); } - - if (logger.isDebugEnabled()) { - for (String listenerName : beforeTestMethodCalls) { - logger.debug("'before' listener [" + listenerName + "] (" + usageContext + ")."); - } - for (String listenerName : afterTestMethodCalls) { - logger.debug("'after' listener [" + listenerName + "] (" + usageContext + ")."); - } + catch (Exception ex) { + throw new RuntimeException(ex); } - - assertTrue("Verifying execution order of 'before' listeners' (" + usageContext + ").", - expectedBeforeTestMethodCalls.equals(beforeTestMethodCalls)); - assertTrue("Verifying execution order of 'after' listeners' (" + usageContext + ").", - expectedAfterTestMethodCalls.equals(afterTestMethodCalls)); } - @BeforeClass - public static void setUpBeforeClass() throws Exception { - beforeTestMethodCalls.clear(); - afterTestMethodCalls.clear(); - assertExecutionOrder(null, null, "BeforeClass"); - } - - /** - * Verifies the expected {@link TestExecutionListener} - * execution order after all test methods have completed. - */ - @AfterClass - public static void verifyListenerExecutionOrderAfterClass() throws Exception { - assertExecutionOrder(Arrays. asList(FIRST, SECOND, THIRD), - Arrays. asList(THIRD, SECOND, FIRST), "AfterClass"); - } - @Before - public void setUpTestContextManager() throws Throwable { - assertEquals("Verifying the number of registered TestExecutionListeners.", 3, - this.testContextManager.getTestExecutionListeners().size()); - - this.testContextManager.beforeTestMethod(new ExampleTestCase(), getTestMethod()); - } - - /** - * Verifies the expected {@link TestExecutionListener} - * execution order within a test method. - * - * @see #verifyListenerExecutionOrderAfterClass() - */ @Test - public void verifyListenerExecutionOrderWithinTestMethod() { - assertExecutionOrder(Arrays. asList(FIRST, SECOND, THIRD), null, "Test"); + public void listenerExecutionOrder() throws Exception { + // @formatter:off + assertEquals("Registered TestExecutionListeners", 3, this.testContextManager.getTestExecutionListeners().size()); + + this.testContextManager.beforeTestMethod(this, this.testMethod); + assertExecutionOrder("beforeTestMethod", + "beforeTestMethod-1", + "beforeTestMethod-2", + "beforeTestMethod-3" + ); + + this.testContextManager.beforeTestExecution(this, this.testMethod); + assertExecutionOrder("beforeTestExecution", + "beforeTestMethod-1", + "beforeTestMethod-2", + "beforeTestMethod-3", + "beforeTestExecution-1", + "beforeTestExecution-2", + "beforeTestExecution-3" + ); + + this.testContextManager.afterTestExecution(this, this.testMethod, null); + assertExecutionOrder("afterTestExecution", + "beforeTestMethod-1", + "beforeTestMethod-2", + "beforeTestMethod-3", + "beforeTestExecution-1", + "beforeTestExecution-2", + "beforeTestExecution-3", + "afterTestExecution-3", + "afterTestExecution-2", + "afterTestExecution-1" + ); + + this.testContextManager.afterTestMethod(this, this.testMethod, null); + assertExecutionOrder("afterTestMethod", + "beforeTestMethod-1", + "beforeTestMethod-2", + "beforeTestMethod-3", + "beforeTestExecution-1", + "beforeTestExecution-2", + "beforeTestExecution-3", + "afterTestExecution-3", + "afterTestExecution-2", + "afterTestExecution-1", + "afterTestMethod-3", + "afterTestMethod-2", + "afterTestMethod-1" + ); + // @formatter:on } - @After - public void tearDownTestContextManager() throws Throwable { - this.testContextManager.afterTestMethod(new ExampleTestCase(), getTestMethod(), null); + private static void assertExecutionOrder(String usageContext, String... expectedBeforeTestMethodCalls) { + assertEquals("execution order (" + usageContext + ") ==>", Arrays.asList(expectedBeforeTestMethodCalls), + executionOrder); } @@ -140,53 +114,57 @@ public class TestContextManagerTests { @SuppressWarnings("unused") public void exampleTestMethod() { - assertTrue(true); } } - private static class NamedTestExecutionListener extends AbstractTestExecutionListener { + private static class NamedTestExecutionListener implements TestExecutionListener { private final String name; - public NamedTestExecutionListener(final String name) { + public NamedTestExecutionListener(String name) { this.name = name; } @Override - public void beforeTestMethod(final TestContext testContext) { - beforeTestMethodCalls.add(this.name); + public void beforeTestMethod(TestContext testContext) { + executionOrder.add("beforeTestMethod-" + this.name); + } + + @Override + public void beforeTestExecution(TestContext testContext) { + executionOrder.add("beforeTestExecution-" + this.name); } @Override - public void afterTestMethod(final TestContext testContext) { - afterTestMethodCalls.add(this.name); + public void afterTestExecution(TestContext testContext) { + executionOrder.add("afterTestExecution-" + this.name); } @Override - public String toString() { - return new ToStringCreator(this).append("name", this.name).toString(); + public void afterTestMethod(TestContext testContext) { + executionOrder.add("afterTestMethod-" + this.name); } } private static class FirstTel extends NamedTestExecutionListener { public FirstTel() { - super(FIRST); + super("1"); } } private static class SecondTel extends NamedTestExecutionListener { public SecondTel() { - super(SECOND); + super("2"); } } private static class ThirdTel extends NamedTestExecutionListener { public ThirdTel() { - super(THIRD); + super("3"); } }