Skip to content

Commit b5fb743

Browse files
committed
Test exception handling for TestContext event annotations
This commit introduces tests for both synchronous and asynchronous exception handling for TestContext event annotations. See gh-18490
1 parent 6853d50 commit b5fb743

File tree

1 file changed

+165
-32
lines changed

1 file changed

+165
-32
lines changed

spring-test/src/test/java/org/springframework/test/context/event/EventPublishingTestExecutionListenerIntegrationTests.java

Lines changed: 165 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,26 @@
1717
package org.springframework.test.context.event;
1818

1919
import java.lang.annotation.Retention;
20+
import java.lang.annotation.Target;
2021
import java.lang.reflect.Method;
22+
import java.util.concurrent.CountDownLatch;
23+
import java.util.concurrent.Executor;
24+
import java.util.concurrent.TimeUnit;
2125

2226
import org.junit.Before;
27+
import org.junit.Rule;
2328
import org.junit.Test;
29+
import org.junit.rules.ExpectedException;
30+
import org.junit.runner.RunWith;
2431

32+
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
2533
import org.springframework.context.annotation.Bean;
2634
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.scheduling.annotation.Async;
36+
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
37+
import org.springframework.scheduling.annotation.EnableAsync;
38+
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
39+
import org.springframework.stereotype.Component;
2740
import org.springframework.test.context.ContextConfiguration;
2841
import org.springframework.test.context.TestContext;
2942
import org.springframework.test.context.TestContextManager;
@@ -36,14 +49,20 @@
3649
import org.springframework.test.context.event.annotation.BeforeTestExecution;
3750
import org.springframework.test.context.event.annotation.BeforeTestMethod;
3851
import org.springframework.test.context.event.annotation.PrepareTestInstance;
52+
import org.springframework.test.context.junit4.SpringRunner;
3953
import org.springframework.util.ReflectionUtils;
4054

55+
import static java.lang.annotation.ElementType.METHOD;
4156
import static java.lang.annotation.RetentionPolicy.RUNTIME;
57+
import static org.hamcrest.CoreMatchers.equalTo;
58+
import static org.hamcrest.CoreMatchers.startsWith;
59+
import static org.junit.Assert.assertThat;
4260
import static org.mockito.Mockito.mock;
4361
import static org.mockito.Mockito.never;
4462
import static org.mockito.Mockito.only;
4563
import static org.mockito.Mockito.reset;
4664
import static org.mockito.Mockito.verify;
65+
import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS;
4766

4867
/**
4968
* Integration tests for {@link EventPublishingTestExecutionListener} and
@@ -55,11 +74,18 @@
5574
*/
5675
public class EventPublishingTestExecutionListenerIntegrationTests {
5776

77+
private static final String THREAD_NAME_PREFIX = "Test-";
78+
79+
private static final CountDownLatch countDownLatch = new CountDownLatch(1);
80+
5881
private final TestContextManager testContextManager = new TestContextManager(ExampleTestCase.class);
5982
private final TestContext testContext = testContextManager.getTestContext();
60-
private final TestExecutionListener listener = testContext.getApplicationContext().getBean(EventCaptureConfiguration.class).listener();
83+
private final TestExecutionListener listener = testContext.getApplicationContext().getBean(TestExecutionListener.class);
6184
private final Object testInstance = new ExampleTestCase();
62-
private final Method testMethod = ReflectionUtils.findMethod(ExampleTestCase.class, "test1");
85+
private final Method testMethod = ReflectionUtils.findMethod(ExampleTestCase.class, "traceableTest");
86+
87+
@Rule
88+
public final ExpectedException exception = ExpectedException.none();
6389

6490

6591
@Before
@@ -82,18 +108,49 @@ public void prepareTestInstanceAnnotation() throws Exception {
82108
}
83109

84110
@Test
85-
public void beforeTestMethodAnnotationWithMatchingCondition() throws Exception {
111+
public void beforeTestMethodAnnotation() throws Exception {
86112
testContextManager.beforeTestMethod(testInstance, testMethod);
87113
verify(listener, only()).beforeTestMethod(testContext);
88114
}
89115

90116
@Test
91117
public void beforeTestMethodAnnotationWithFailingCondition() throws Exception {
92-
Method testMethod2 = ReflectionUtils.findMethod(ExampleTestCase.class, "test2");
93-
testContextManager.beforeTestMethod(testInstance, testMethod2);
118+
Method standardTest = ReflectionUtils.findMethod(ExampleTestCase.class, "standardTest");
119+
testContextManager.beforeTestMethod(testInstance, standardTest);
94120
verify(listener, never()).beforeTestMethod(testContext);
95121
}
96122

123+
@Test
124+
public void beforeTestMethodAnnotationWithFailingEventListener() throws Exception {
125+
Method method = ReflectionUtils.findMethod(ExampleTestCase.class, "testWithFailingEventListener");
126+
127+
exception.expect(RuntimeException.class);
128+
exception.expectMessage("Boom!");
129+
130+
try {
131+
testContextManager.beforeTestMethod(testInstance, method);
132+
}
133+
finally {
134+
verify(listener, only()).beforeTestMethod(testContext);
135+
}
136+
}
137+
138+
@Test
139+
public void beforeTestMethodAnnotationWithFailingAsyncEventListener() throws Exception {
140+
TrackingAsyncUncaughtExceptionHandler.asyncException = null;
141+
142+
String methodName = "testWithFailingAsyncEventListener";
143+
Method method = ReflectionUtils.findMethod(ExampleTestCase.class, methodName);
144+
145+
testContextManager.beforeTestMethod(testInstance, method);
146+
147+
assertThat(countDownLatch.await(2, TimeUnit.SECONDS), equalTo(true));
148+
149+
verify(listener, only()).beforeTestMethod(testContext);
150+
assertThat(TrackingAsyncUncaughtExceptionHandler.asyncException.getMessage(),
151+
startsWith("Asynchronous exception for test method [" + methodName + "] in thread [" + THREAD_NAME_PREFIX));
152+
}
153+
97154
@Test
98155
public void beforeTestExecutionAnnotation() throws Exception {
99156
testContextManager.beforeTestExecution(testInstance, testMethod);
@@ -119,66 +176,142 @@ public void afterTestClassAnnotation() throws Exception {
119176
}
120177

121178

179+
@Target(METHOD)
180+
@Retention(RUNTIME)
181+
@interface Traceable {
182+
}
183+
184+
@RunWith(SpringRunner.class)
185+
@ContextConfiguration(classes = EventCaptureConfiguration.class)
186+
@TestExecutionListeners(listeners = EventPublishingTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
187+
public static class ExampleTestCase {
188+
189+
@Traceable
190+
@Test
191+
public void traceableTest() {
192+
/* no-op */
193+
}
194+
195+
@Test
196+
public void standardTest() {
197+
/* no-op */
198+
}
199+
200+
@Test
201+
public void testWithFailingEventListener() {
202+
/* no-op */
203+
}
204+
205+
@Test
206+
public void testWithFailingAsyncEventListener() {
207+
/* no-op */
208+
}
209+
210+
}
211+
122212
@Configuration
123-
static class EventCaptureConfiguration {
213+
@EnableAsync(proxyTargetClass = true)
214+
static class EventCaptureConfiguration extends AsyncConfigurerSupport {
215+
216+
@Override
217+
public Executor getAsyncExecutor() {
218+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
219+
executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
220+
executor.initialize();
221+
return executor;
222+
}
223+
224+
@Override
225+
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
226+
return new TrackingAsyncUncaughtExceptionHandler();
227+
}
124228

125229
@Bean
126230
public TestExecutionListener listener() {
127231
return mock(TestExecutionListener.class);
128232
}
129233

234+
@Bean
235+
EventCaptureBean eventCaptureBean() {
236+
return new EventCaptureBean(listener());
237+
}
238+
239+
}
240+
241+
static class TrackingAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
242+
243+
static volatile Throwable asyncException;
244+
245+
246+
@Override
247+
public void handleUncaughtException(Throwable exception, Method method, Object... params) {
248+
asyncException = exception;
249+
countDownLatch.countDown();
250+
}
251+
}
252+
253+
// MUST be annotated with @Component due to a change in Spring 5.1 that
254+
// does not consider beans in a package starting with "org.springframework"
255+
// to be event listeners unless they are also components. See
256+
// org.springframework.context.event.EventListenerMethodProcessor.isSpringContainerClass(Class<?>)
257+
// for details.
258+
@Component
259+
static class EventCaptureBean {
260+
261+
final TestExecutionListener listener;
262+
263+
264+
EventCaptureBean(TestExecutionListener listener) {
265+
this.listener = listener;
266+
}
267+
130268
@BeforeTestClass("#root.event.source.testClass.name matches '.+TestCase'")
131269
public void beforeTestClass(BeforeTestClassEvent e) throws Exception {
132-
listener().beforeTestClass(e.getSource());
270+
this.listener.beforeTestClass(e.getSource());
133271
}
134272

135273
@PrepareTestInstance("#a0.testContext.testClass.name matches '.+TestCase'")
136274
public void prepareTestInstance(PrepareTestInstanceEvent e) throws Exception {
137-
listener().prepareTestInstance(e.getSource());
275+
this.listener.prepareTestInstance(e.getSource());
138276
}
139277

140278
@BeforeTestMethod("#p0.testContext.testMethod.isAnnotationPresent(T(org.springframework.test.context.event.EventPublishingTestExecutionListenerIntegrationTests.Traceable))")
141279
public void beforeTestMethod(BeforeTestMethodEvent e) throws Exception {
142-
listener().beforeTestMethod(e.getSource());
280+
this.listener.beforeTestMethod(e.getSource());
281+
}
282+
283+
@BeforeTestMethod("event.testContext.testMethod.name == 'testWithFailingEventListener'")
284+
public void beforeTestMethodWithFailure(BeforeTestMethodEvent event) throws Exception {
285+
this.listener.beforeTestMethod(event.getSource());
286+
throw new RuntimeException("Boom!");
287+
}
288+
289+
@BeforeTestMethod("event.testContext.testMethod.name == 'testWithFailingAsyncEventListener'")
290+
@Async
291+
public void beforeTestMethodWithAsyncFailure(BeforeTestMethodEvent event) throws Exception {
292+
this.listener.beforeTestMethod(event.getSource());
293+
throw new RuntimeException(String.format("Asynchronous exception for test method [%s] in thread [%s]",
294+
event.getTestContext().getTestMethod().getName(), Thread.currentThread().getName()));
143295
}
144296

145297
@BeforeTestExecution
146298
public void beforeTestExecution(BeforeTestExecutionEvent e) throws Exception {
147-
listener().beforeTestExecution(e.getSource());
299+
this.listener.beforeTestExecution(e.getSource());
148300
}
149301

150302
@AfterTestExecution
151303
public void afterTestExecution(AfterTestExecutionEvent e) throws Exception {
152-
listener().afterTestExecution(e.getSource());
304+
this.listener.afterTestExecution(e.getSource());
153305
}
154306

155307
@AfterTestMethod("event.testContext.testMethod.isAnnotationPresent(T(org.springframework.test.context.event.EventPublishingTestExecutionListenerIntegrationTests.Traceable))")
156308
public void afterTestMethod(AfterTestMethodEvent e) throws Exception {
157-
listener().afterTestMethod(e.getSource());
309+
this.listener.afterTestMethod(e.getSource());
158310
}
159311

160312
@AfterTestClass("#afterTestClassEvent.testContext.testClass.name matches '.+TestCase'")
161313
public void afterTestClass(AfterTestClassEvent afterTestClassEvent) throws Exception {
162-
listener().afterTestClass(afterTestClassEvent.getSource());
163-
}
164-
165-
}
166-
167-
@Retention(RUNTIME)
168-
@interface Traceable {
169-
}
170-
171-
@ContextConfiguration(classes = EventCaptureConfiguration.class)
172-
@TestExecutionListeners(EventPublishingTestExecutionListener.class)
173-
static class ExampleTestCase {
174-
175-
@Traceable
176-
public void test1() {
177-
/* no-op */
178-
}
179-
180-
public void test2() {
181-
/* no-op */
314+
this.listener.afterTestClass(afterTestClassEvent.getSource());
182315
}
183316
}
184317

0 commit comments

Comments
 (0)