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 446bd58dfde..a2413b1b39f 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/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java index 74fe7fc401f..c2c71537006 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -55,6 +55,7 @@ package org.springframework.test.context; * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 + * @see TestContextManager * @see org.springframework.test.context.support.AbstractTestExecutionListener */ public interface TestExecutionListener { @@ -70,6 +71,7 @@ public interface TestExecutionListener { * concrete classes as necessary. * @param testContext the test context for the test; never {@code null} * @throws Exception allows any exception to propagate + * @since 3.0 */ default void beforeTestClass(TestContext testContext) throws Exception { /* no-op */ @@ -90,34 +92,90 @@ public interface TestExecutionListener { } /** - * Pre-processes a test before execution of the + * Pre-processes a test before execution of before + * lifecycle callbacks of the underlying test framework — for example, + * by setting up test fixtures. + *

    This method must be called immediately prior to + * framework-specific before lifecycle callbacks. 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 default implementation is empty. Can be overridden by + * concrete classes as necessary. + * @param testContext the test context in which the test method will be + * executed; never {@code null} + * @throws Exception allows any exception to propagate + * @see #afterTestMethod + * @see #beforeTestExecution + * @see #afterTestExecution + */ + default void beforeTestMethod(TestContext testContext) throws Exception { + /* no-op */ + } + + /** + * Pre-processes a test immediately before execution of the * {@link java.lang.reflect.Method test method} in the supplied - * {@link TestContext test context}, for example by setting up test - * fixtures. - *

    This method should be called immediately prior to framework-specific + * {@link TestContext test context} — for example, for timing + * or logging purposes. + *

    This method must be called after framework-specific * before lifecycle callbacks. *

    The default implementation is empty. Can be overridden by * concrete classes as necessary. * @param testContext the test context in which the test method will be * executed; never {@code null} * @throws Exception allows any exception to propagate + * @since 5.0 + * @see #beforeTestMethod + * @see #afterTestMethod + * @see #afterTestExecution */ - default void beforeTestMethod(TestContext testContext) throws Exception { + default void beforeTestExecution(TestContext testContext) throws Exception { /* no-op */ } /** - * Post-processes a test after execution of the + * Post-processes a test immediately after execution of the * {@link java.lang.reflect.Method test method} in the supplied - * {@link TestContext test context}, for example by tearing down test - * fixtures. - *

    This method should be called immediately after framework-specific + * {@link TestContext test context} — for example, for timing + * or logging purposes. + *

    This method must be called before framework-specific * after lifecycle callbacks. *

    The default implementation is empty. Can be overridden by * concrete classes as necessary. + * @param testContext the test context in which the test method will be + * executed; never {@code null} + * @throws Exception allows any exception to propagate + * @since 5.0 + * @see #beforeTestMethod + * @see #afterTestMethod + * @see #beforeTestExecution + */ + default void afterTestExecution(TestContext testContext) throws Exception { + /* no-op */ + } + + /** + * Post-processes a test after execution of after + * lifecycle callbacks of the underlying test framework — for example, + * by tearing down test fixtures. + *

    This method must be called immediately after + * framework-specific after lifecycle callbacks. 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 default implementation is empty. Can be overridden by + * concrete classes as necessary. * @param testContext the test context in which the test method was * executed; never {@code null} * @throws Exception allows any exception to propagate + * @see #beforeTestMethod + * @see #beforeTestExecution + * @see #afterTestExecution */ default void afterTestMethod(TestContext testContext) throws Exception { /* no-op */ @@ -134,6 +192,7 @@ public interface TestExecutionListener { * concrete classes as necessary. * @param testContext the test context for the test; never {@code null} * @throws Exception allows any exception to propagate + * @since 3.0 */ default void afterTestClass(TestContext testContext) throws Exception { /* no-op */ diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index a8a91f40687..8eef966a330 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -23,8 +23,10 @@ import java.lang.reflect.Parameter; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ContainerExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; @@ -54,7 +56,8 @@ import org.springframework.util.Assert; * @see org.springframework.test.context.TestContextManager */ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor, - BeforeEachCallback, AfterEachCallback, ParameterResolver { + BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, + ParameterResolver { /** * {@link Namespace} in which {@code TestContextManagers} are stored, keyed @@ -101,6 +104,27 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes getTestContextManager(context).beforeTestMethod(testInstance, testMethod); } + /** + * Delegates to {@link TestContextManager#beforeTestExecution}. + */ + @Override + public void beforeTestExecution(TestExtensionContext context) throws Exception { + Object testInstance = context.getTestInstance(); + Method testMethod = context.getTestMethod().get(); + getTestContextManager(context).beforeTestExecution(testInstance, testMethod); + } + + /** + * Delegates to {@link TestContextManager#afterTestExecution}. + */ + @Override + public void afterTestExecution(TestExtensionContext context) throws Exception { + Object testInstance = context.getTestInstance(); + Method testMethod = context.getTestMethod().get(); + Throwable testException = context.getTestException().orElse(null); + getTestContextManager(context).afterTestExecution(testInstance, testMethod, testException); + } + /** * Delegates to {@link TestContextManager#afterTestMethod}. */ diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java index a0ed3db9c20..22fa1a136d2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java @@ -42,8 +42,10 @@ import org.springframework.test.context.TestContextManager; import org.springframework.test.context.junit4.rules.SpringClassRule; import org.springframework.test.context.junit4.rules.SpringMethodRule; import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks; +import org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks; import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks; import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks; +import org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks; import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks; import org.springframework.test.context.junit4.statements.SpringFailOnTimeout; import org.springframework.test.context.junit4.statements.SpringRepeat; @@ -270,6 +272,9 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { * Spring-specific timeouts in that the former execute in a separate * thread while the latter simply execute in the main thread (like regular * tests). + * @see #methodInvoker(FrameworkMethod, Object) + * @see #withBeforeTestExecutionCallbacks(FrameworkMethod, Object, Statement) + * @see #withAfterTestExecutionCallbacks(FrameworkMethod, Object, Statement) * @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement) * @see #withBefores(FrameworkMethod, Object, Statement) * @see #withAfters(FrameworkMethod, Object, Statement) @@ -293,6 +298,8 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { } Statement statement = methodInvoker(frameworkMethod, testInstance); + statement = withBeforeTestExecutionCallbacks(frameworkMethod, testInstance, statement); + statement = withAfterTestExecutionCallbacks(frameworkMethod, testInstance, statement); statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement); statement = withBefores(frameworkMethod, testInstance, statement); statement = withAfters(frameworkMethod, testInstance, statement); @@ -404,6 +411,26 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { return TestAnnotationUtils.getTimeout(frameworkMethod.getMethod()); } + /** + * Wrap the supplied {@link Statement} with a {@code RunBeforeTestExecutionCallbacks} + * statement, thus preserving the default functionality while adding support for the + * Spring TestContext Framework. + * @see RunBeforeTestExecutionCallbacks + */ + protected Statement withBeforeTestExecutionCallbacks(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { + return new RunBeforeTestExecutionCallbacks(statement, testInstance, frameworkMethod.getMethod(), getTestContextManager()); + } + + /** + * Wrap the supplied {@link Statement} with a {@code RunAfterTestExecutionCallbacks} + * statement, thus preserving the default functionality while adding support for the + * Spring TestContext Framework. + * @see RunAfterTestExecutionCallbacks + */ + protected Statement withAfterTestExecutionCallbacks(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { + return new RunAfterTestExecutionCallbacks(statement, testInstance, frameworkMethod.getMethod(), getTestContextManager()); + } + /** * Wrap the {@link Statement} returned by the parent implementation with a * {@code RunBeforeTestMethodCallbacks} statement, thus preserving the @@ -414,8 +441,7 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { @Override protected Statement withBefores(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement); - return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(), - getTestContextManager()); + return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(), getTestContextManager()); } /** @@ -428,8 +454,7 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { @Override protected Statement withAfters(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) { Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement); - return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(), - getTestContextManager()); + return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(), getTestContextManager()); } /** diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java index 89a97418ac1..a8f78a0d06b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -37,7 +37,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; /** - * {@code SpringMethodRule} is a custom JUnit {@link MethodRule} that + * {@code SpringMethodRule} is a custom JUnit 4 {@link MethodRule} that * supports instance-level and method-level features of the * Spring TestContext Framework in standard JUnit tests by means * of the {@link TestContextManager} and associated support classes and @@ -82,6 +82,12 @@ import org.springframework.util.ReflectionUtils; * *

    NOTE: As of Spring Framework 4.3, this class requires JUnit 4.12 or higher. * + *

    WARNING: Due to the shortcomings of JUnit rules, the + * {@code SpringMethodRule} does not support the + * {@code beforeTestExecution()} and {@code afterTestExecution()} callbacks of the + * {@link org.springframework.test.context.TestExecutionListener TestExecutionListener} + * API. + * * @author Sam Brannen * @author Philippe Marschall * @since 4.2 diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestExecutionCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestExecutionCallbacks.java new file mode 100644 index 00000000000..818b1e8b3c3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestExecutionCallbacks.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2016 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.context.junit4.statements; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.runners.model.MultipleFailureException; +import org.junit.runners.model.Statement; +import org.springframework.test.context.TestContextManager; + +/** + * {@code RunAfterTestExecutionCallbacks} is a custom JUnit {@link Statement} + * which allows the Spring TestContext Framework to be plugged into the + * JUnit 4 execution chain by calling {@link TestContextManager#afterTestExecution + * afterTestExecution()} on the supplied {@link TestContextManager}. + * + *

    NOTE: This class requires JUnit 4.9 or higher. + * + * @author Sam Brannen + * @since 5.0 + * @see #evaluate() + * @see RunBeforeTestExecutionCallbacks + */ +public class RunAfterTestExecutionCallbacks extends Statement { + + private final Statement next; + + private final Object testInstance; + + private final Method testMethod; + + private final TestContextManager testContextManager; + + + /** + * Construct a new {@code RunAfterTestExecutionCallbacks} statement. + * @param next the next {@code Statement} in the execution chain + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which has just been executed on the + * test instance + * @param testContextManager the TestContextManager upon which to call + * {@code afterTestExecution()} + */ + public RunAfterTestExecutionCallbacks(Statement next, Object testInstance, Method testMethod, + TestContextManager testContextManager) { + + this.next = next; + this.testInstance = testInstance; + this.testMethod = testMethod; + this.testContextManager = testContextManager; + } + + /** + * Evaluate the next {@link Statement} in the execution chain (typically an + * instance of {@link RunBeforeTestExecutionCallbacks}), catching any exceptions + * thrown, and then invoke {@link TestContextManager#afterTestExecution} supplying + * the first caught exception (if any). + *

    If the invocation of {@code afterTestExecution()} throws an exception, that + * exception will also be tracked. Multiple exceptions will be combined into a + * {@link MultipleFailureException}. + */ + @Override + public void evaluate() throws Throwable { + Throwable testException = null; + List errors = new ArrayList<>(); + try { + this.next.evaluate(); + } + catch (Throwable ex) { + testException = ex; + errors.add(ex); + } + + try { + this.testContextManager.afterTestExecution(this.testInstance, this.testMethod, testException); + } + catch (Throwable ex) { + errors.add(ex); + } + + MultipleFailureException.assertEmpty(errors); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestExecutionCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestExecutionCallbacks.java new file mode 100644 index 00000000000..338ec27ed2d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunBeforeTestExecutionCallbacks.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2016 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.context.junit4.statements; + +import java.lang.reflect.Method; + +import org.junit.runners.model.Statement; +import org.springframework.test.context.TestContextManager; + +/** + * {@code RunBeforeTestExecutionCallbacks} is a custom JUnit {@link Statement} + * which allows the Spring TestContext Framework to be plugged into the + * JUnit 4 execution chain by calling {@link TestContextManager#beforeTestExecution + * beforeTestExecution()} on the supplied {@link TestContextManager}. + * + * @author Sam Brannen + * @since 5.0 + * @see #evaluate() + * @see RunAfterTestExecutionCallbacks + */ +public class RunBeforeTestExecutionCallbacks extends Statement { + + private final Statement next; + + private final Object testInstance; + + private final Method testMethod; + + private final TestContextManager testContextManager; + + + /** + * Construct a new {@code RunBeforeTestExecutionCallbacks} statement. + * @param next the next {@code Statement} in the execution chain + * @param testInstance the current test instance (never {@code null}) + * @param testMethod the test method which is about to be executed on the + * test instance + * @param testContextManager the TestContextManager upon which to call + * {@code beforeTestExecution()} + */ + public RunBeforeTestExecutionCallbacks(Statement next, Object testInstance, Method testMethod, + TestContextManager testContextManager) { + + this.next = next; + this.testInstance = testInstance; + this.testMethod = testMethod; + this.testContextManager = testContextManager; + } + + /** + * Invoke {@link TestContextManager#beforeTestExecution(Object, Method)} + * and then evaluate the next {@link Statement} in the execution chain + * (typically an instance of + * {@link org.junit.internal.runners.statements.InvokeMethod InvokeMethod}). + */ + @Override + public void evaluate() throws Throwable { + this.testContextManager.beforeTestExecution(this.testInstance, this.testMethod); + this.next.evaluate(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java index 2be1967b386..fcffa7f3fcc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 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. @@ -49,12 +49,11 @@ import org.testng.annotations.BeforeMethod; *

    Concrete subclasses: *