Skip to content

Commit a1d15c9

Browse files
authored
Wait for @AfterEach method's VertxTestContext in case a @test method failed (#148)
* Wait for @AfterEach method's VertxTestContext in case a @test method failed * Rework test to be run programmatically from another test * Rework test to be run programmatically from another test * Some reformatting, imports expansions * Fix typo in disabled annotation comment * Clean up comments
1 parent 710662e commit a1d15c9

File tree

3 files changed

+139
-3
lines changed

3 files changed

+139
-3
lines changed

src/main/java/io/vertx/junit5/VertxExtension.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@
1212
package io.vertx.junit5;
1313

1414
import io.vertx.core.Vertx;
15+
import org.junit.jupiter.api.AfterEach;
1516
import org.junit.jupiter.api.Nested;
16-
import org.junit.jupiter.api.extension.*;
17+
import org.junit.jupiter.api.extension.DynamicTestInvocationContext;
18+
import org.junit.jupiter.api.extension.ExtensionContext;
1719
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
1820
import org.junit.jupiter.api.extension.ExtensionContext.Store;
21+
import org.junit.jupiter.api.extension.InvocationInterceptor;
22+
import org.junit.jupiter.api.extension.ParameterContext;
23+
import org.junit.jupiter.api.extension.ParameterResolutionException;
24+
import org.junit.jupiter.api.extension.ParameterResolver;
25+
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
1926

2027
import java.lang.reflect.Method;
2128
import java.util.ArrayList;
@@ -165,12 +172,23 @@ public void interceptDynamicTest(Invocation<Void> invocation, DynamicTestInvocat
165172
@Override
166173
public void interceptAfterEachMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
167174
invocation.proceed();
168-
joinActiveTestContexts(extensionContext);
175+
joinActiveTestContexts(invocationContext, extensionContext);
169176
}
170177

171178
private void joinActiveTestContexts(ExtensionContext extensionContext) throws Exception {
179+
joinActiveTestContexts(null, extensionContext);
180+
}
181+
182+
private void joinActiveTestContexts(ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Exception {
172183
if (extensionContext.getExecutionException().isPresent()) {
173-
return;
184+
final boolean isNotInAfterEachMethod = Optional.ofNullable(invocationContext)
185+
.map(ReflectiveInvocationContext::getExecutable)
186+
.map(executable -> executable.getAnnotation(AfterEach.class))
187+
.isEmpty();
188+
189+
if (isNotInAfterEachMethod) {
190+
return;
191+
}
174192
}
175193

176194
ContextList currentContexts = store(extensionContext).remove(TEST_CONTEXT_KEY, ContextList.class);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.vertx.junit5.tests;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.platform.engine.DiscoverySelector;
5+
import org.junit.platform.engine.TestExecutionResult;
6+
import org.junit.platform.engine.discovery.DiscoverySelectors;
7+
import org.junit.platform.engine.support.descriptor.ClassSource;
8+
import org.junit.platform.launcher.EngineFilter;
9+
import org.junit.platform.launcher.Launcher;
10+
import org.junit.platform.launcher.LauncherDiscoveryRequest;
11+
import org.junit.platform.launcher.TestExecutionListener;
12+
import org.junit.platform.launcher.TestIdentifier;
13+
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
14+
import org.junit.platform.launcher.core.LauncherFactory;
15+
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
16+
import org.junit.platform.launcher.listeners.TestExecutionSummary;
17+
18+
import java.util.concurrent.atomic.AtomicReference;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
class RunAfterEachContextCheckTest {
23+
24+
@Test
25+
void runsWaitForContextInAfterEachMethodTestAndChecksAfterAllSucceeded() {
26+
// Select only the target class
27+
final DiscoverySelector selector = DiscoverySelectors.selectClass(WaitForContextInAfterEachMethodTest.class);
28+
29+
// Collect a summary for assertions
30+
final SummaryGeneratingListener summaryListener = new SummaryGeneratingListener();
31+
32+
// Capture the class container result specifically (to assert @AfterAll behavior)
33+
final AtomicReference<TestExecutionResult.Status> classStatus = new AtomicReference<>();
34+
35+
final TestExecutionListener captureClassStatus = new TestExecutionListener() {
36+
@Override
37+
public void executionFinished(final TestIdentifier id, final TestExecutionResult result) {
38+
id.getSource()
39+
.ifPresent(source -> {
40+
if (id.isContainer() && source instanceof ClassSource) {
41+
final ClassSource cs = (ClassSource) source;
42+
if (cs.getClassName().equals(WaitForContextInAfterEachMethodTest.class.getName())) {
43+
classStatus.set(result.getStatus());
44+
}
45+
}
46+
});
47+
}
48+
};
49+
50+
final LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
51+
.selectors(selector)
52+
.filters(EngineFilter.includeEngines("junit-jupiter"))
53+
// Make @Disabled inert for this run
54+
.configurationParameter(
55+
"junit.jupiter.conditions.deactivate", "org.junit.*DisabledCondition")
56+
// Make execution deterministic
57+
.configurationParameter("junit.jupiter.execution.parallel.enabled", "false")
58+
.build();
59+
60+
final Launcher launcher = LauncherFactory.create();
61+
launcher.registerTestExecutionListeners(summaryListener, captureClassStatus);
62+
63+
launcher.execute(request);
64+
65+
final TestExecutionSummary summary = summaryListener.getSummary();
66+
67+
// The single test method intentionally fails
68+
assertEquals(1, summary.getTestsFoundCount(), "Expect exactly one test discovered");
69+
assertEquals(1, summary.getTestsFailedCount(), "The test should fail via context.failNow()");
70+
assertEquals(0, summary.getContainersFailedCount(), "The test class container should not fail");
71+
72+
// Critically: the class container (where @AfterAll runs) must be SUCCESSFUL
73+
assertEquals(TestExecutionResult.Status.SUCCESSFUL,
74+
classStatus.get(),
75+
"@AfterAll must complete its VertxTestContext without failure");
76+
}
77+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.vertx.junit5.tests;
2+
3+
import io.vertx.core.Vertx;
4+
import io.vertx.junit5.VertxExtension;
5+
import io.vertx.junit5.VertxTestContext;
6+
import org.junit.jupiter.api.AfterAll;
7+
import org.junit.jupiter.api.AfterEach;
8+
import org.junit.jupiter.api.Disabled;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.ExtendWith;
11+
12+
import java.util.concurrent.atomic.AtomicBoolean;
13+
14+
@Disabled("Executed only via programmatic launcher from RunAfterEachContextCheckTest")
15+
@ExtendWith({VertxExtension.class})
16+
public class WaitForContextInAfterEachMethodTest {
17+
static final AtomicBoolean afterTestContextAwaited = new AtomicBoolean(false);
18+
19+
@Test
20+
void test(final VertxTestContext context) {
21+
context.failNow(new RuntimeException("Failing test through context"));
22+
}
23+
24+
@AfterAll
25+
static void afterAll(final VertxTestContext context) {
26+
if (afterTestContextAwaited.get()) {
27+
context.completeNow();
28+
} else {
29+
context.failNow(new RuntimeException("afterTest context was not awaited"));
30+
}
31+
}
32+
33+
@AfterEach
34+
void afterTest(final Vertx vertx, final VertxTestContext context) {
35+
vertx.setTimer(100, id -> {
36+
afterTestContextAwaited.set(true);
37+
context.completeNow();
38+
});
39+
}
40+
41+
}

0 commit comments

Comments
 (0)