Skip to content

Commit 087efa6

Browse files
committed
Introduce before/after test execution callbacks in TestContextManager
Issue: SPR-4365
1 parent 5302566 commit 087efa6

File tree

2 files changed

+273
-159
lines changed

2 files changed

+273
-159
lines changed

spring-test/src/main/java/org/springframework/test/context/TestContextManager.java

Lines changed: 197 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,29 @@
4040
* <li>{@link #beforeTestClass() before test class execution}: prior to any
4141
* <em>before class callbacks</em> of a particular testing framework (e.g.,
4242
* JUnit 4's {@link org.junit.BeforeClass @BeforeClass})</li>
43-
* <li>{@link #prepareTestInstance(Object) test instance preparation}:
44-
* immediately following instantiation of the test instance</li>
45-
* <li>{@link #beforeTestMethod(Object, Method) before test method execution}:
43+
* <li>{@link #prepareTestInstance test instance preparation}:
44+
* immediately following instantiation of the test class</li>
45+
* <li>{@link #beforeTestMethod before test setup}:
4646
* prior to any <em>before method callbacks</em> of a particular testing framework
4747
* (e.g., JUnit 4's {@link org.junit.Before @Before})</li>
48-
* <li>{@link #afterTestMethod(Object, Method, Throwable) after test method
49-
* execution}: after any <em>after method callbacks</em> of a particular testing
48+
* <li>{@link #beforeTestExecution before test execution}:
49+
* immediately before execution of the {@linkplain java.lang.reflect.Method
50+
* test method} but after test setup</li>
51+
* <li>{@link #afterTestExecution after test execution}:
52+
* immediately after execution of the {@linkplain java.lang.reflect.Method
53+
* test method} but before test tear down</li>
54+
* <li>{@link #afterTestMethod(Object, Method, Throwable) after test tear down}:
55+
* after any <em>after method callbacks</em> of a particular testing
5056
* framework (e.g., JUnit 4's {@link org.junit.After @After})</li>
5157
* <li>{@link #afterTestClass() after test class execution}: after any
52-
* <em>after class callbacks</em> of a particular testing framework (e.g., JUnit
53-
* 4's {@link org.junit.AfterClass @AfterClass})</li>
58+
* <em>after class callbacks</em> of a particular testing framework (e.g., JUnit 4's
59+
* {@link org.junit.AfterClass @AfterClass})</li>
5460
* </ul>
5561
*
5662
* <p>Support for loading and accessing
57-
* {@link org.springframework.context.ApplicationContext application contexts},
63+
* {@linkplain org.springframework.context.ApplicationContext application contexts},
5864
* dependency injection of test instances,
59-
* {@link org.springframework.transaction.annotation.Transactional transactional}
65+
* {@linkplain org.springframework.transaction.annotation.Transactional transactional}
6066
* execution of test methods, etc. is provided by
6167
* {@link SmartContextLoader ContextLoaders} and {@link TestExecutionListener
6268
* TestExecutionListeners}, which are configured via
@@ -173,14 +179,15 @@ private List<TestExecutionListener> getReversedTestExecutionListeners() {
173179
/**
174180
* Hook for pre-processing a test class <em>before</em> execution of any
175181
* tests within the class. Should be called prior to any framework-specific
176-
* <em>before class methods</em> (e.g., methods annotated with JUnit's
182+
* <em>before class methods</em> (e.g., methods annotated with JUnit 4's
177183
* {@link org.junit.BeforeClass @BeforeClass}).
178184
* <p>An attempt will be made to give each registered
179185
* {@link TestExecutionListener} a chance to pre-process the test class
180186
* execution. If a listener throws an exception, however, the remaining
181187
* registered listeners will <strong>not</strong> be called.
182188
* @throws Exception if a registered TestExecutionListener throws an
183189
* exception
190+
* @since 3.0
184191
* @see #getTestExecutionListeners()
185192
*/
186193
public void beforeTestClass() throws Exception {
@@ -195,10 +202,7 @@ public void beforeTestClass() throws Exception {
195202
testExecutionListener.beforeTestClass(getTestContext());
196203
}
197204
catch (Throwable ex) {
198-
if (logger.isWarnEnabled()) {
199-
logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
200-
"] to process 'before class' callback for test class [" + testClass + "]", ex);
201-
}
205+
logException(ex, "beforeTestClass", testExecutionListener, testClass);
202206
ReflectionUtils.rethrowException(ex);
203207
}
204208
}
@@ -240,76 +244,172 @@ public void prepareTestInstance(Object testInstance) throws Exception {
240244
}
241245

242246
/**
243-
* Hook for pre-processing a test <em>before</em> execution of the supplied
244-
* {@link Method test method}, for example for setting up test fixtures,
245-
* starting a transaction, etc. Should be called prior to any
246-
* framework-specific <em>before methods</em> (e.g., methods annotated with
247-
* JUnit's {@link org.junit.Before @Before}).
247+
* Hook for pre-processing a test <em>before</em> execution of <em>before</em>
248+
* lifecycle callbacks of the underlying test framework &mdash; for example,
249+
* setting up test fixtures, starting a transaction, etc.
250+
* <p>This method <strong>must</strong> be called immediately prior to
251+
* framework-specific <em>before</em> lifecycle callbacks (e.g., methods
252+
* annotated with JUnit 4's {@link org.junit.Before @Before}). For historical
253+
* reasons, this method is named {@code beforeTestMethod}. Since the
254+
* introduction of {@link #beforeTestExecution}, a more suitable name for
255+
* this method might be something like {@code beforeTestSetUp} or
256+
* {@code beforeEach}; however, it is unfortunately impossible to rename
257+
* this method due to backward compatibility concerns.
248258
* <p>The managed {@link TestContext} will be updated with the supplied
249259
* {@code testInstance} and {@code testMethod}.
250260
* <p>An attempt will be made to give each registered
251-
* {@link TestExecutionListener} a chance to pre-process the test method
252-
* execution. If a listener throws an exception, however, the remaining
253-
* registered listeners will <strong>not</strong> be called.
261+
* {@link TestExecutionListener} a chance to perform its pre-processing.
262+
* If a listener throws an exception, however, the remaining registered
263+
* listeners will <strong>not</strong> be called.
254264
* @param testInstance the current test instance (never {@code null})
255265
* @param testMethod the test method which is about to be executed on the
256266
* test instance
257267
* @throws Exception if a registered TestExecutionListener throws an exception
268+
* @see #afterTestMethod
269+
* @see #beforeTestExecution
270+
* @see #afterTestExecution
258271
* @see #getTestExecutionListeners()
259272
*/
260273
public void beforeTestMethod(Object testInstance, Method testMethod) throws Exception {
261-
Assert.notNull(testInstance, "Test instance must not be null");
262-
if (logger.isTraceEnabled()) {
263-
logger.trace("beforeTestMethod(): instance [" + testInstance + "], method [" + testMethod + "]");
264-
}
265-
getTestContext().updateState(testInstance, testMethod, null);
274+
String callbackName = "beforeTestMethod";
275+
prepareForBeforeCallback(callbackName, testInstance, testMethod);
266276

267277
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
268278
try {
269279
testExecutionListener.beforeTestMethod(getTestContext());
270280
}
271281
catch (Throwable ex) {
272-
if (logger.isWarnEnabled()) {
273-
logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
274-
"] to process 'before' execution of test method [" + testMethod + "] for test instance [" +
275-
testInstance + "]", ex);
276-
}
277-
ReflectionUtils.rethrowException(ex);
282+
handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod);
283+
}
284+
}
285+
}
286+
287+
/**
288+
* Hook for pre-processing a test <em>immediately before</em> execution of
289+
* the {@linkplain java.lang.reflect.Method test method} in the supplied
290+
* {@linkplain TestContext test context} &mdash; for example, for timing
291+
* or logging purposes.
292+
* <p>This method <strong>must</strong> be called after framework-specific
293+
* <em>before</em> lifecycle callbacks (e.g., methods annotated with JUnit 4's
294+
* {@link org.junit.Before @Before}).
295+
* <p>The managed {@link TestContext} will be updated with the supplied
296+
* {@code testInstance} and {@code testMethod}.
297+
* <p>An attempt will be made to give each registered
298+
* {@link TestExecutionListener} a chance to perform its pre-processing.
299+
* If a listener throws an exception, however, the remaining registered
300+
* listeners will <strong>not</strong> be called.
301+
* @param testInstance the current test instance (never {@code null})
302+
* @param testMethod the test method which is about to be executed on the
303+
* test instance
304+
* @throws Exception if a registered TestExecutionListener throws an exception
305+
* @since 5.0
306+
* @see #beforeTestMethod
307+
* @see #afterTestMethod
308+
* @see #beforeTestExecution
309+
* @see #afterTestExecution
310+
* @see #getTestExecutionListeners()
311+
*/
312+
public void beforeTestExecution(Object testInstance, Method testMethod) throws Exception {
313+
String callbackName = "beforeTestExecution";
314+
prepareForBeforeCallback(callbackName, testInstance, testMethod);
315+
316+
for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
317+
try {
318+
testExecutionListener.beforeTestExecution(getTestContext());
319+
}
320+
catch (Throwable ex) {
321+
handleBeforeException(ex, callbackName, testExecutionListener, testInstance, testMethod);
278322
}
279323
}
280324
}
281325

282326
/**
283-
* Hook for post-processing a test <em>after</em> execution of the supplied
284-
* {@link Method test method}, for example for tearing down test fixtures,
285-
* ending a transaction, etc. Should be called after any framework-specific
286-
* <em>after methods</em> (e.g., methods annotated with JUnit's
327+
* Hook for post-processing a test <em>immediately after</em> execution of
328+
* the {@linkplain java.lang.reflect.Method test method} in the supplied
329+
* {@linkplain TestContext test context} &mdash; for example, for timing
330+
* or logging purposes.
331+
* <p>This method <strong>must</strong> be called before framework-specific
332+
* <em>after</em> lifecycle callbacks (e.g., methods annotated with JUnit 4's
287333
* {@link org.junit.After @After}).
288334
* <p>The managed {@link TestContext} will be updated with the supplied
289-
* {@code testInstance}, {@code testMethod}, and
290-
* {@code exception}.
291-
* <p>Each registered {@link TestExecutionListener} will be given a chance to
292-
* post-process the test method execution. If a listener throws an
293-
* exception, the remaining registered listeners will still be called, but
294-
* the first exception thrown will be tracked and rethrown after all
295-
* listeners have executed. Note that registered listeners will be executed
296-
* in the opposite order in which they were registered.
335+
* {@code testInstance}, {@code testMethod}, and {@code exception}.
336+
* <p>Each registered {@link TestExecutionListener} will be given a chance
337+
* to perform its post-processing. If a listener throws an exception, the
338+
* remaining registered listeners will still be called, but the first
339+
* exception thrown will be tracked and rethrown after all listeners have
340+
* executed. Note that registered listeners will be executed in the opposite
341+
* order in which they were registered.
297342
* @param testInstance the current test instance (never {@code null})
298343
* @param testMethod the test method which has just been executed on the
299344
* test instance
300345
* @param exception the exception that was thrown during execution of the
301346
* test method or by a TestExecutionListener, or {@code null} if none
302347
* was thrown
303348
* @throws Exception if a registered TestExecutionListener throws an exception
349+
* @since 5.0
350+
* @see #beforeTestMethod
351+
* @see #afterTestMethod
352+
* @see #beforeTestExecution
304353
* @see #getTestExecutionListeners()
305354
*/
306-
public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception {
307-
Assert.notNull(testInstance, "Test instance must not be null");
308-
if (logger.isTraceEnabled()) {
309-
logger.trace("afterTestMethod(): instance [" + testInstance + "], method [" + testMethod +
310-
"], exception [" + exception + "]");
355+
public void afterTestExecution(Object testInstance, Method testMethod, Throwable exception) throws Exception {
356+
String callbackName = "afterTestExecution";
357+
prepareForAfterCallback(callbackName, testInstance, testMethod, exception);
358+
359+
Throwable afterTestExecutionException = null;
360+
// Traverse the TestExecutionListeners in reverse order to ensure proper
361+
// "wrapper"-style execution of listeners.
362+
for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) {
363+
try {
364+
testExecutionListener.afterTestExecution(getTestContext());
365+
}
366+
catch (Throwable ex) {
367+
logException(ex, callbackName, testExecutionListener, testInstance, testMethod);
368+
if (afterTestExecutionException == null) {
369+
afterTestExecutionException = ex;
370+
}
371+
}
311372
}
312-
getTestContext().updateState(testInstance, testMethod, exception);
373+
if (afterTestExecutionException != null) {
374+
ReflectionUtils.rethrowException(afterTestExecutionException);
375+
}
376+
}
377+
378+
/**
379+
* Hook for post-processing a test <em>after</em> execution of <em>after</em>
380+
* lifecycle callbacks of the underlying test framework &mdash; for example,
381+
* tearing down test fixtures, ending a transaction, etc.
382+
* <p>This method <strong>must</strong> be called immediately after
383+
* framework-specific <em>after</em> lifecycle callbacks (e.g., methods
384+
* annotated with JUnit 4's {@link org.junit.After @After}). For historical
385+
* reasons, this method is named {@code afterTestMethod}. Since the
386+
* introduction of {@link #afterTestExecution}, a more suitable name for
387+
* this method might be something like {@code afterTestTearDown} or
388+
* {@code afterEach}; however, it is unfortunately impossible to rename
389+
* this method due to backward compatibility concerns.
390+
* <p>The managed {@link TestContext} will be updated with the supplied
391+
* {@code testInstance}, {@code testMethod}, and {@code exception}.
392+
* <p>Each registered {@link TestExecutionListener} will be given a chance
393+
* to perform its post-processing. If a listener throws an exception, the
394+
* remaining registered listeners will still be called, but the first
395+
* exception thrown will be tracked and rethrown after all listeners have
396+
* executed. Note that registered listeners will be executed in the opposite
397+
* order in which they were registered.
398+
* @param testInstance the current test instance (never {@code null})
399+
* @param testMethod the test method which has just been executed on the
400+
* test instance
401+
* @param exception the exception that was thrown during execution of the
402+
* test method or by a TestExecutionListener, or {@code null} if none
403+
* was thrown
404+
* @throws Exception if a registered TestExecutionListener throws an exception
405+
* @see #beforeTestMethod
406+
* @see #beforeTestExecution
407+
* @see #afterTestExecution
408+
* @see #getTestExecutionListeners()
409+
*/
410+
public void afterTestMethod(Object testInstance, Method testMethod, Throwable exception) throws Exception {
411+
String callbackName = "afterTestMethod";
412+
prepareForAfterCallback(callbackName, testInstance, testMethod, exception);
313413

314414
Throwable afterTestMethodException = null;
315415
// Traverse the TestExecutionListeners in reverse order to ensure proper
@@ -319,11 +419,7 @@ public void afterTestMethod(Object testInstance, Method testMethod, Throwable ex
319419
testExecutionListener.afterTestMethod(getTestContext());
320420
}
321421
catch (Throwable ex) {
322-
if (logger.isWarnEnabled()) {
323-
logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
324-
"] to process 'after' execution for test: method [" + testMethod + "], instance [" +
325-
testInstance + "], exception [" + exception + "]", ex);
326-
}
422+
logException(ex, callbackName, testExecutionListener, testInstance, testMethod);
327423
if (afterTestMethodException == null) {
328424
afterTestMethodException = ex;
329425
}
@@ -337,7 +433,7 @@ public void afterTestMethod(Object testInstance, Method testMethod, Throwable ex
337433
/**
338434
* Hook for post-processing a test class <em>after</em> execution of all
339435
* tests within the class. Should be called after any framework-specific
340-
* <em>after class methods</em> (e.g., methods annotated with JUnit's
436+
* <em>after class methods</em> (e.g., methods annotated with JUnit 4's
341437
* {@link org.junit.AfterClass @AfterClass}).
342438
* <p>Each registered {@link TestExecutionListener} will be given a chance to
343439
* post-process the test class. If a listener throws an exception, the
@@ -346,6 +442,7 @@ public void afterTestMethod(Object testInstance, Method testMethod, Throwable ex
346442
* executed. Note that registered listeners will be executed in the opposite
347443
* order in which they were registered.
348444
* @throws Exception if a registered TestExecutionListener throws an exception
445+
* @since 3.0
349446
* @see #getTestExecutionListeners()
350447
*/
351448
public void afterTestClass() throws Exception {
@@ -363,10 +460,7 @@ public void afterTestClass() throws Exception {
363460
testExecutionListener.afterTestClass(getTestContext());
364461
}
365462
catch (Throwable ex) {
366-
if (logger.isWarnEnabled()) {
367-
logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener +
368-
"] to process 'after class' callback for test class [" + testClass + "]", ex);
369-
}
463+
logException(ex, "afterTestClass", testExecutionListener, testClass);
370464
if (afterTestClassException == null) {
371465
afterTestClassException = ex;
372466
}
@@ -377,4 +471,46 @@ public void afterTestClass() throws Exception {
377471
}
378472
}
379473

474+
private void prepareForBeforeCallback(String callbackName, Object testInstance, Method testMethod) {
475+
Assert.notNull(testInstance, "Test instance must not be null");
476+
if (logger.isTraceEnabled()) {
477+
logger.trace(String.format("%s(): instance [%s], method [%s]", callbackName, testInstance, testMethod));
478+
}
479+
getTestContext().updateState(testInstance, testMethod, null);
480+
}
481+
482+
private void prepareForAfterCallback(String callbackName, Object testInstance, Method testMethod,
483+
Throwable exception) {
484+
Assert.notNull(testInstance, "Test instance must not be null");
485+
if (logger.isTraceEnabled()) {
486+
logger.trace(String.format("%s(): instance [%s], method [%s], exception [%s]", callbackName, testInstance,
487+
testMethod, exception));
488+
}
489+
getTestContext().updateState(testInstance, testMethod, exception);
490+
}
491+
492+
private void handleBeforeException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener,
493+
Object testInstance, Method testMethod) throws Exception {
494+
logException(ex, callbackName, testExecutionListener, testInstance, testMethod);
495+
ReflectionUtils.rethrowException(ex);
496+
}
497+
498+
private void logException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener,
499+
Class<?> testClass) {
500+
if (logger.isWarnEnabled()) {
501+
logger.warn(String.format("Caught exception while invoking '%s' callback on " +
502+
"TestExecutionListener [%s] for test class [%s]", callbackName, testExecutionListener,
503+
testClass), ex);
504+
}
505+
}
506+
507+
private void logException(Throwable ex, String callbackName, TestExecutionListener testExecutionListener,
508+
Object testInstance, Method testMethod) {
509+
if (logger.isWarnEnabled()) {
510+
logger.warn(String.format("Caught exception while invoking '%s' callback on " +
511+
"TestExecutionListener [%s] for test method [%s] and test instance [%s]",
512+
callbackName, testExecutionListener, testMethod, testInstance), ex);
513+
}
514+
}
515+
380516
}

0 commit comments

Comments
 (0)