Skip to content

Commit f42aede

Browse files
authored
Introduce ExtensionContext.getTestInstances() API
The new `getTestInstances()` and `getRequiredTestInstances()` methods of `ExtensionContext` allow accessing all test instances, including enclosing ones for `@Nested` tests. Resolves #1618.
1 parent c804c80 commit f42aede

File tree

21 files changed

+495
-172
lines changed

21 files changed

+495
-172
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.4.0-M1.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ repository on GitHub.
132132
methods via the `junit-jupiter-migrationsupport` module.
133133
- See the <<../user-guide/index.adoc#migrating-from-junit4-ignore-annotation-support,
134134
User Guide>> for details.
135+
* New `ExtensionContext` methods to access all test instances, including enclosing ones
136+
for `@Nested` tests: `getTestInstances()` and `getRequiredTestInstances()`.
135137

136138

137139
[[release-notes-5.4.0-M1-junit-vintage]]

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
package org.junit.jupiter.api.extension;
1212

13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1314
import static org.apiguardian.api.API.Status.STABLE;
1415

1516
import java.lang.reflect.AnnotatedElement;
@@ -160,6 +161,7 @@ default Class<?> getRequiredTestClass() {
160161
* @return an {@code Optional} containing the test instance; never
161162
* {@code null} but potentially empty
162163
* @see #getRequiredTestInstance()
164+
* @see #getTestInstances()
163165
*/
164166
Optional<Object> getTestInstance();
165167

@@ -173,12 +175,49 @@ default Class<?> getRequiredTestClass() {
173175
* @return the test instance; never {@code null}
174176
* @throws PreconditionViolationException if the test instance is not present
175177
* in this {@code ExtensionContext}
178+
*
179+
* @see #getRequiredTestInstances()
176180
*/
177181
default Object getRequiredTestInstance() {
178182
return Preconditions.notNull(getTestInstance().orElse(null),
179183
"Illegal state: required test instance is not present in the current ExtensionContext");
180184
}
181185

186+
/**
187+
* Get the test instances associated with the current test or container,
188+
* if available.
189+
*
190+
* <p>While top-level tests only have a single test instance, nested tests
191+
* have one additional instance for each enclosing test class.
192+
*
193+
* @return an {@code Optional} containing the test instances; never
194+
* {@code null} but potentially empty
195+
* @see #getRequiredTestInstances()
196+
*
197+
* @since 5.4
198+
*/
199+
@API(status = EXPERIMENTAL, since = "5.4")
200+
Optional<TestInstances> getTestInstances();
201+
202+
/**
203+
* Get the <em>required</em> test instances associated with the current test
204+
* or container.
205+
*
206+
* <p>Use this method as an alternative to {@link #getTestInstances()} for use
207+
* cases in which the test instances are required to be present.
208+
*
209+
* @return the test instances; never {@code null}
210+
* @throws PreconditionViolationException if the test instances are not present
211+
* in this {@code ExtensionContext}
212+
*
213+
* @since 5.4
214+
*/
215+
@API(status = EXPERIMENTAL, since = "5.4")
216+
default TestInstances getRequiredTestInstances() {
217+
return Preconditions.notNull(getTestInstances().orElse(null),
218+
"Illegal state: required test instances are not present in the current ExtensionContext");
219+
}
220+
182221
/**
183222
* Get the {@link Method} associated with the current test, if available.
184223
*
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2015-2018 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* http://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api.extension;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import java.util.List;
16+
import java.util.Optional;
17+
18+
import org.apiguardian.api.API;
19+
20+
/**
21+
* {@code TestInstances} encapsulates the <em>test instances</em> of a test.
22+
*
23+
* <p>While top-level tests only have a single test instance, nested tests
24+
* have one additional instance for each enclosing test class.
25+
*
26+
* @since 5.4
27+
* @see ExtensionContext#getTestInstances()
28+
* @see ExtensionContext#getRequiredTestInstances()
29+
*/
30+
@API(status = EXPERIMENTAL, since = "5.4")
31+
public interface TestInstances {
32+
33+
/**
34+
* Get the innermost test instance.
35+
*
36+
* <p>The innermost instance is the one closest to the test method.
37+
*
38+
* @return the innermost test instance; never {@code null}
39+
*/
40+
Object getInnermostInstance();
41+
42+
/**
43+
* Get the enclosing test instances, excluding the innermost test instance,
44+
* ordered from outermost to innermost.
45+
*
46+
* @return the enclosing test instances; never {@code null} or containing
47+
* {@code null}, but potentially empty
48+
*/
49+
List<Object> getEnclosingInstances();
50+
51+
/**
52+
* Get all test instances, ordered from outermost to innermost.
53+
*
54+
* @return all test instances; never {@code null}, containing {@code null},
55+
* or empty
56+
*/
57+
List<Object> getAllInstances();
58+
59+
/**
60+
* Find the first test instance that is an instance of the supplied required
61+
* type, checking from innermost to outermost.
62+
*
63+
* @param requiredType the type to search for
64+
* @return the first test instance of the required type; never {@code null}
65+
* but potentially empty
66+
*/
67+
<T> Optional<T> findInstance(Class<T> requiredType);
68+
69+
}

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.apiguardian.api.API;
2020
import org.junit.jupiter.api.TestInstance.Lifecycle;
2121
import org.junit.jupiter.api.extension.ExtensionContext;
22+
import org.junit.jupiter.api.extension.TestInstances;
2223
import org.junit.jupiter.engine.config.JupiterConfiguration;
2324
import org.junit.platform.engine.EngineExecutionListener;
2425
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
@@ -33,7 +34,7 @@ public final class ClassExtensionContext extends AbstractExtensionContext<ClassT
3334

3435
private final ThrowableCollector throwableCollector;
3536

36-
private Object testInstance;
37+
private TestInstances testInstances;
3738

3839
/**
3940
* Create a new {@code ClassExtensionContext} with {@link Lifecycle#PER_METHOD}.
@@ -72,13 +73,18 @@ public Optional<Lifecycle> getTestInstanceLifecycle() {
7273
return Optional.of(this.lifecycle);
7374
}
7475

75-
void setTestInstance(Object testInstance) {
76-
this.testInstance = testInstance;
76+
@Override
77+
public Optional<Object> getTestInstance() {
78+
return getTestInstances().map(TestInstances::getInnermostInstance);
7779
}
7880

7981
@Override
80-
public Optional<Object> getTestInstance() {
81-
return Optional.ofNullable(this.testInstance);
82+
public Optional<TestInstances> getTestInstances() {
83+
return Optional.ofNullable(testInstances);
84+
}
85+
86+
void setTestInstances(TestInstances testInstances) {
87+
this.testInstances = testInstances;
8288
}
8389

8490
@Override

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@
4242
import org.junit.jupiter.api.extension.ExtensionContext;
4343
import org.junit.jupiter.api.extension.TestInstanceFactory;
4444
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
45+
import org.junit.jupiter.api.extension.TestInstances;
4546
import org.junit.jupiter.api.extension.TestInstantiationException;
4647
import org.junit.jupiter.engine.config.JupiterConfiguration;
4748
import org.junit.jupiter.engine.execution.AfterEachMethodAdapter;
4849
import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter;
50+
import org.junit.jupiter.engine.execution.DefaultTestInstances;
4951
import org.junit.jupiter.engine.execution.ExecutableInvoker;
5052
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
51-
import org.junit.jupiter.engine.execution.TestInstanceProvider;
53+
import org.junit.jupiter.engine.execution.TestInstancesProvider;
5254
import org.junit.jupiter.engine.extension.ExtensionRegistry;
5355
import org.junit.platform.commons.JUnitException;
5456
import org.junit.platform.commons.util.BlacklistedExceptions;
@@ -169,7 +171,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
169171

170172
// @formatter:off
171173
return context.extend()
172-
.withTestInstanceProvider(testInstanceProvider(context, registry, extensionContext))
174+
.withTestInstancesProvider(testInstancesProvider(context, registry, extensionContext))
173175
.withExtensionRegistry(registry)
174176
.withExtensionContext(extensionContext)
175177
.withThrowableCollector(throwableCollector)
@@ -186,8 +188,8 @@ public JupiterEngineExecutionContext before(JupiterEngineExecutionContext contex
186188
// Eagerly load test instance for BeforeAllCallbacks, if necessary,
187189
// and store the instance in the ExtensionContext.
188190
ClassExtensionContext extensionContext = (ClassExtensionContext) context.getExtensionContext();
189-
throwableCollector.execute(() -> extensionContext.setTestInstance(
190-
context.getTestInstanceProvider().getTestInstance(Optional.empty())));
191+
throwableCollector.execute(() -> extensionContext.setTestInstances(
192+
context.getTestInstancesProvider().getTestInstances(Optional.empty())));
191193
}
192194

193195
if (throwableCollector.isEmpty()) {
@@ -251,40 +253,43 @@ private TestInstanceFactory resolveTestInstanceFactory(ExtensionRegistry registr
251253
return null;
252254
}
253255

254-
private TestInstanceProvider testInstanceProvider(JupiterEngineExecutionContext parentExecutionContext,
256+
private TestInstancesProvider testInstancesProvider(JupiterEngineExecutionContext parentExecutionContext,
255257
ExtensionRegistry registry, ClassExtensionContext extensionContext) {
256258

257-
TestInstanceProvider testInstanceProvider = childRegistry -> instantiateAndPostProcessTestInstance(
259+
TestInstancesProvider testInstancesProvider = childRegistry -> instantiateAndPostProcessTestInstance(
258260
parentExecutionContext, extensionContext, childRegistry.orElse(registry));
259261

260-
return childRegistry -> extensionContext.getTestInstance().orElseGet(
261-
() -> testInstanceProvider.getTestInstance(childRegistry));
262+
return childRegistry -> extensionContext.getTestInstances().orElseGet(
263+
() -> testInstancesProvider.getTestInstances(childRegistry));
262264
}
263265

264-
private Object instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
266+
private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
265267
ExtensionContext extensionContext, ExtensionRegistry registry) {
266268

267-
Object instance = instantiateTestClass(parentExecutionContext, registry, extensionContext);
268-
invokeTestInstancePostProcessors(instance, registry, extensionContext);
269+
TestInstances instances = instantiateTestClass(parentExecutionContext, registry, extensionContext);
270+
invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, extensionContext);
269271
// In addition, we register extensions from instance fields here since the
270272
// best time to do that is immediately following test class instantiation
271273
// and post processing.
272-
registerExtensionsFromFields(registry, this.testClass, instance);
273-
return instance;
274+
registerExtensionsFromFields(registry, this.testClass, instances.getInnermostInstance());
275+
return instances;
274276
}
275277

276-
protected Object instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
278+
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
277279
ExtensionRegistry registry, ExtensionContext extensionContext) {
278280

279281
return instantiateTestClass(Optional.empty(), registry, extensionContext);
280282
}
281283

282-
protected Object instantiateTestClass(Optional<Object> outerInstance, ExtensionRegistry registry,
284+
protected TestInstances instantiateTestClass(Optional<TestInstances> outerInstances, ExtensionRegistry registry,
283285
ExtensionContext extensionContext) {
284286

285-
return this.testInstanceFactory != null //
287+
Optional<Object> outerInstance = outerInstances.map(TestInstances::getInnermostInstance);
288+
Object instance = this.testInstanceFactory != null //
286289
? invokeTestInstanceFactory(outerInstance, extensionContext) //
287290
: invokeTestClassConstructor(outerInstance, registry, extensionContext);
291+
return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)).orElse(
292+
DefaultTestInstances.of(instance));
288293
}
289294

290295
private Object invokeTestInstanceFactory(Optional<Object> outerInstance, ExtensionContext extensionContext) {
@@ -427,11 +432,11 @@ private AfterEachMethodAdapter synthesizeAfterEachMethodAdapter(Method method) {
427432
}
428433

429434
private void invokeMethodInExtensionContext(Method method, ExtensionContext context, ExtensionRegistry registry) {
430-
Object testInstance = context.getRequiredTestInstance();
431-
testInstance = ReflectionUtils.getOutermostInstance(testInstance, method.getDeclaringClass()).orElseThrow(
435+
TestInstances testInstances = context.getRequiredTestInstances();
436+
Object target = testInstances.findInstance(method.getDeclaringClass()).orElseThrow(
432437
() -> new JUnitException("Failed to find instance for method: " + method.toGenericString()));
433438

434-
executableInvoker.invoke(method, testInstance, context, registry);
439+
executableInvoker.invoke(method, target, context, registry);
435440
}
436441

437442
}

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import org.apiguardian.api.API;
2020
import org.junit.jupiter.api.TestInstance.Lifecycle;
21+
import org.junit.jupiter.api.extension.TestInstances;
2122
import org.junit.jupiter.engine.config.JupiterConfiguration;
2223
import org.junit.platform.engine.EngineExecutionListener;
2324

@@ -53,6 +54,11 @@ public Optional<Object> getTestInstance() {
5354
return Optional.empty();
5455
}
5556

57+
@Override
58+
public Optional<TestInstances> getTestInstances() {
59+
return Optional.empty();
60+
}
61+
5662
@Override
5763
public Optional<Method> getTestMethod() {
5864
return Optional.empty();

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.apiguardian.api.API;
2020
import org.junit.jupiter.api.TestInstance.Lifecycle;
2121
import org.junit.jupiter.api.extension.ExtensionContext;
22+
import org.junit.jupiter.api.extension.TestInstances;
2223
import org.junit.jupiter.engine.config.JupiterConfiguration;
2324
import org.junit.platform.engine.EngineExecutionListener;
2425
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
@@ -29,17 +30,17 @@
2930
@API(status = INTERNAL, since = "5.0")
3031
public final class MethodExtensionContext extends AbstractExtensionContext<TestMethodTestDescriptor> {
3132

32-
private final Object testInstance;
33+
private final TestInstances testInstances;
3334

3435
private final ThrowableCollector throwableCollector;
3536

3637
public MethodExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
37-
TestMethodTestDescriptor testDescriptor, JupiterConfiguration configuration, Object testInstance,
38+
TestMethodTestDescriptor testDescriptor, JupiterConfiguration configuration, TestInstances testInstances,
3839
ThrowableCollector throwableCollector) {
3940

4041
super(parent, engineExecutionListener, testDescriptor, configuration);
4142

42-
this.testInstance = testInstance;
43+
this.testInstances = testInstances;
4344
this.throwableCollector = throwableCollector;
4445
}
4546

@@ -60,7 +61,12 @@ public Optional<Lifecycle> getTestInstanceLifecycle() {
6061

6162
@Override
6263
public Optional<Object> getTestInstance() {
63-
return Optional.of(this.testInstance);
64+
return Optional.of(this.testInstances.getInnermostInstance());
65+
}
66+
67+
@Override
68+
public Optional<TestInstances> getTestInstances() {
69+
return Optional.of(this.testInstances);
6470
}
6571

6672
@Override

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.apiguardian.api.API;
2121
import org.junit.jupiter.api.extension.ExtensionContext;
22+
import org.junit.jupiter.api.extension.TestInstances;
2223
import org.junit.jupiter.engine.config.JupiterConfiguration;
2324
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
2425
import org.junit.jupiter.engine.extension.ExtensionRegistry;
@@ -63,14 +64,14 @@ public final Set<TestTag> getTags() {
6364
// --- Node ----------------------------------------------------------------
6465

6566
@Override
66-
protected Object instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
67+
protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext,
6768
ExtensionRegistry registry, ExtensionContext extensionContext) {
6869

6970
// Extensions registered for nested classes and below are not to be used for instantiating outer classes
7071
Optional<ExtensionRegistry> childExtensionRegistryForOuterInstance = Optional.empty();
71-
Object outerInstance = parentExecutionContext.getTestInstanceProvider().getTestInstance(
72+
TestInstances outerInstances = parentExecutionContext.getTestInstancesProvider().getTestInstances(
7273
childExtensionRegistryForOuterInstance);
73-
return instantiateTestClass(Optional.of(outerInstance), registry, extensionContext);
74+
return instantiateTestClass(Optional.of(outerInstances), registry, extensionContext);
7475
}
7576

7677
}

0 commit comments

Comments
 (0)