|
1 | 1 | /*
|
2 |
| - * Copyright 2002-2021 the original author or authors. |
| 2 | + * Copyright 2002-2023 the original author or authors. |
3 | 3 | *
|
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | 5 | * you may not use this file except in compliance with the License.
|
|
18 | 18 |
|
19 | 19 | import java.util.Set;
|
20 | 20 | import java.util.concurrent.ConcurrentHashMap;
|
| 21 | +import java.util.concurrent.ExecutorService; |
| 22 | +import java.util.concurrent.Executors; |
| 23 | +import java.util.concurrent.TimeUnit; |
21 | 24 | import java.util.stream.Collectors;
|
22 | 25 | import java.util.stream.Stream;
|
23 | 26 |
|
| 27 | +import org.assertj.core.api.InstanceOfAssertFactories; |
| 28 | +import org.awaitility.Awaitility; |
| 29 | +import org.awaitility.Durations; |
24 | 30 | import org.junit.jupiter.api.AfterEach;
|
25 | 31 | import org.junit.jupiter.api.Test;
|
26 | 32 | import org.junit.jupiter.api.TestInfo;
|
27 | 33 | import org.junit.jupiter.api.TestInstance;
|
28 | 34 | import org.junit.jupiter.api.TestInstance.Lifecycle;
|
29 | 35 | import org.junit.jupiter.api.parallel.Execution;
|
30 | 36 | import org.junit.jupiter.api.parallel.ExecutionMode;
|
31 |
| -import org.junit.jupiter.params.ParameterizedTest; |
32 |
| -import org.junit.jupiter.params.provider.ValueSource; |
| 37 | +import org.junit.platform.engine.TestExecutionResult; |
| 38 | +import org.junit.platform.testkit.engine.EngineExecutionResults; |
33 | 39 | import org.junit.platform.testkit.engine.EngineTestKit;
|
| 40 | +import org.junit.platform.testkit.engine.Events; |
34 | 41 |
|
35 | 42 | import org.springframework.beans.factory.annotation.Autowired;
|
36 | 43 | import org.springframework.context.ApplicationContext;
|
| 44 | +import org.springframework.context.PayloadApplicationEvent; |
37 | 45 | import org.springframework.context.annotation.Configuration;
|
38 | 46 | import org.springframework.test.context.event.ApplicationEvents;
|
| 47 | +import org.springframework.test.context.event.ApplicationEventsHolder; |
39 | 48 | import org.springframework.test.context.event.RecordApplicationEvents;
|
| 49 | +import org.springframework.test.context.event.TestContextEvent; |
40 | 50 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
41 | 51 |
|
42 | 52 | import static org.assertj.core.api.Assertions.assertThat;
|
|
47 | 57 | * in conjunction with JUnit Jupiter.
|
48 | 58 | *
|
49 | 59 | * @author Sam Brannen
|
| 60 | + * @author Simon Baslé |
50 | 61 | * @since 5.3.3
|
51 | 62 | */
|
52 | 63 | class ParallelApplicationEventsIntegrationTests {
|
53 | 64 |
|
54 | 65 | private static final Set<String> payloads = ConcurrentHashMap.newKeySet();
|
55 | 66 |
|
| 67 | + @Test |
| 68 | + void rejectTestsInParallelWithInstancePerClassAndRecordApplicationEvents() { |
| 69 | + Class<?> testClass = TestInstancePerClassTestCase.class; |
56 | 70 |
|
57 |
| - @ParameterizedTest |
58 |
| - @ValueSource(classes = {TestInstancePerMethodTestCase.class, TestInstancePerClassTestCase.class}) |
59 |
| - void executeTestsInParallel(Class<?> testClass) { |
60 |
| - EngineTestKit.engine("junit-jupiter")// |
| 71 | + final EngineExecutionResults results = EngineTestKit.engine("junit-jupiter")// |
| 72 | + .selectors(selectClass(testClass))// |
| 73 | + .configurationParameter("junit.jupiter.execution.parallel.enabled", "true")// |
| 74 | + .configurationParameter("junit.jupiter.execution.parallel.config.dynamic.factor", "10")// |
| 75 | + .execute(); |
| 76 | + |
| 77 | + //extract the messages from failed TextExecutionResults |
| 78 | + assertThat(results.containerEvents().failed()// |
| 79 | + .stream().map(e -> e.getRequiredPayload(TestExecutionResult.class)// |
| 80 | + .getThrowable().get().getMessage()))// |
| 81 | + .singleElement(InstanceOfAssertFactories.STRING) |
| 82 | + .isEqualToIgnoringNewLines(""" |
| 83 | + Test classes or inner classes that @RecordApplicationEvents\s |
| 84 | + must not be run in parallel with the @TestInstance(Lifecycle.PER_CLASS) configuration.\s |
| 85 | + Use either @Execution(SAME_THREAD), @TestInstance(PER_METHOD) or disable parallel\s |
| 86 | + execution altogether. Note that when recording events in parallel, one might see events\s |
| 87 | + published by other tests as the application context can be common. |
| 88 | + """); |
| 89 | + } |
| 90 | + |
| 91 | + @Test |
| 92 | + void executeTestsInParallelInstancePerMethod() { |
| 93 | + Class<?> testClass = TestInstancePerMethodTestCase.class; |
| 94 | + Events testEvents = EngineTestKit.engine("junit-jupiter")// |
61 | 95 | .selectors(selectClass(testClass))//
|
62 | 96 | .configurationParameter("junit.jupiter.execution.parallel.enabled", "true")//
|
63 | 97 | .configurationParameter("junit.jupiter.execution.parallel.config.dynamic.factor", "10")//
|
64 | 98 | .execute()//
|
65 |
| - .testEvents()// |
66 |
| - .assertStatistics(stats -> stats.started(10).succeeded(10).failed(0)); |
| 99 | + .testEvents(); |
| 100 | + //list failed events in case of test errors to get a sense of which tests failed |
| 101 | + Events failedTests = testEvents.failed(); |
| 102 | + if (failedTests.count() > 0) { |
| 103 | + failedTests.debug(); |
| 104 | + } |
| 105 | + testEvents.assertStatistics(stats -> stats.started(13).succeeded(13).failed(0)); |
67 | 106 |
|
68 | 107 | Set<String> testNames = payloads.stream()//
|
69 | 108 | .map(payload -> payload.substring(0, payload.indexOf("-")))//
|
@@ -162,6 +201,39 @@ void test10(ApplicationEvents events, TestInfo testInfo) {
|
162 | 201 | assertTestExpectations(events, testInfo);
|
163 | 202 | }
|
164 | 203 |
|
| 204 | + @Test |
| 205 | + void compareToApplicationEventsHolder(ApplicationEvents applicationEvents) { |
| 206 | + ApplicationEvents fromThreadHolder = ApplicationEventsHolder.getRequiredApplicationEvents(); |
| 207 | + assertThat(fromThreadHolder.stream()) |
| 208 | + .hasSameElementsAs(this.events.stream().toList()) |
| 209 | + .hasSameElementsAs(applicationEvents.stream().toList()); |
| 210 | + } |
| 211 | + |
| 212 | + @Test |
| 213 | + void asyncPublication(ApplicationEvents events) throws InterruptedException { |
| 214 | + final ExecutorService executorService = Executors.newSingleThreadExecutor(); |
| 215 | + executorService.execute(() -> this.context.publishEvent("asyncPublication")); |
| 216 | + executorService.shutdown(); |
| 217 | + executorService.awaitTermination(10, TimeUnit.SECONDS); |
| 218 | + |
| 219 | + assertThat(events.stream().filter(e -> !(e instanceof TestContextEvent)) |
| 220 | + .map(e -> (e instanceof PayloadApplicationEvent<?> pae ? pae.getPayload().toString() : e.toString()))) |
| 221 | + .containsExactly("asyncPublication"); |
| 222 | + } |
| 223 | + |
| 224 | + @Test |
| 225 | + void asyncConsumption() { |
| 226 | + this.context.publishEvent("asyncConsumption"); |
| 227 | + |
| 228 | + Awaitility.await().atMost(Durations.ONE_SECOND).untilAsserted(() ->// |
| 229 | + assertThat(ApplicationEventsHolder// |
| 230 | + .getRequiredApplicationEvents()// |
| 231 | + .stream()// |
| 232 | + .filter(e -> !(e instanceof TestContextEvent))// |
| 233 | + .map(e -> (e instanceof PayloadApplicationEvent<?> pae ? pae.getPayload().toString() : e.toString()))// |
| 234 | + ).containsExactly("asyncConsumption")); |
| 235 | + } |
| 236 | + |
165 | 237 | private void assertTestExpectations(ApplicationEvents events, TestInfo testInfo) {
|
166 | 238 | String testName = testInfo.getTestMethod().get().getName();
|
167 | 239 | String threadName = Thread.currentThread().getName();
|
|
0 commit comments