Repository: junit-team/junit5 (language: java)

Issue:
Introduce `ExtensionContext.getEnclosingTestClasses()` for use with `AnnotationSupport`
In #4131, the `AnnotationSupport.findAnnotation(Class, Class, SearchOption)` method was deprecated in favor of the new `AnnotationSupport.findAnnotation(Class, Class, List)` method. When called with `SearchOption.INCLUDE_ENCLOSING_CLASSES` it looks up annotations on the passed class or its _static_ enclosed types. In the context of `@Nested` tests that was not correct, as annotations (such as `@DisplayNameGeneration`) need to be looked up on the _runtime_ enclosing types which differ in case a `@Nested` class is declared in a superclass of the test class.

In order to call the new method, `ExtensionContext` should provide a `getEnclosingTestClasses(): List<Class<?>>` method that can be passed like this:

```java
AnnotationSupport.findAnnotation(
    extensionContext.getRequiredTestClass(),
    MyAnnotation.class,
    extensionContext.getEnclosingTestClasses()
);
```

## Deliverables

- [x] Introduce `ExtensionContext.getEnclosingTestClasses()` method



Discussion / hints:

Fix patch:
diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc
index 19bd43c11a0c..db9492e11d39 100644
--- a/documentation/src/docs/asciidoc/release-notes/index.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/index.adoc
@@ -19,4 +19,6 @@ include::{includedir}/link-attributes.adoc[]
 
 include::{basedir}/release-notes-5.13.0-M1.adoc[]
 
+include::{basedir}/release-notes-5.12.1.adoc[]
+
 include::{basedir}/release-notes-5.12.0.adoc[]
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.1.adoc
new file mode 100644
index 000000000000..6a4303cd6e34
--- /dev/null
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.1.adoc
@@ -0,0 +1,69 @@
+[[release-notes-5.12.1]]
+== 5.12.1
+
+*Date of Release:* ❓
+
+*Scope:* ❓
+
+For a complete list of all _closed_ issues and pull requests for this release, consult the
+link:{junit5-repo}+/milestone/91?closed=1+[5.12.1] milestone page in the JUnit repository
+on GitHub.
+
+
+[[release-notes-5.12.1-junit-platform]]
+=== JUnit Platform
+
+[[release-notes-5.12.1-junit-platform-bug-fixes]]
+==== Bug Fixes
+
+* ❓
+
+[[release-notes-5.12.1-junit-platform-deprecations-and-breaking-changes]]
+==== Deprecations and Breaking Changes
+
+* ❓
+
+[[release-notes-5.12.1-junit-platform-new-features-and-improvements]]
+==== New Features and Improvements
+
+* ❓
+
+
+[[release-notes-5.12.1-junit-jupiter]]
+=== JUnit Jupiter
+
+[[release-notes-5.12.1-junit-jupiter-bug-fixes]]
+==== Bug Fixes
+
+* ❓
+
+[[release-notes-5.12.1-junit-jupiter-deprecations-and-breaking-changes]]
+==== Deprecations and Breaking Changes
+
+* ❓
+
+[[release-notes-5.12.1-junit-jupiter-new-features-and-improvements]]
+==== New Features and Improvements
+
+* New `ExtensionContext.getEnclosingTestClasses()` method to help with migration away from
+  `AnnotationSupport.findAnnotation(Class, Class, SearchOption)` (deprecated since 1.12.0)
+  to `AnnotationSupport.findAnnotation(Class, Class, List)`.
+
+
+[[release-notes-5.12.1-junit-vintage]]
+=== JUnit Vintage
+
+[[release-notes-5.12.1-junit-vintage-bug-fixes]]
+==== Bug Fixes
+
+* ❓
+
+[[release-notes-5.12.1-junit-vintage-deprecations-and-breaking-changes]]
+==== Deprecations and Breaking Changes
+
+* ❓
+
+[[release-notes-5.12.1-junit-vintage-new-features-and-improvements]]
+==== New Features and Improvements
+
+* ❓
diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java
index 3e9e3dd03430..6d047292a98d 100644
--- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java
+++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java
@@ -40,6 +40,8 @@
  * <p>{@link Extension Extensions} are provided an instance of
  * {@code ExtensionContext} to perform their work.
  *
+ * <p>This interface is not intended to be implemented by clients.
+ *
  * @since 5.0
  * @see Store
  * @see Namespace
@@ -129,6 +131,31 @@ public interface ExtensionContext {
 	 */
 	Optional<Class<?>> getTestClass();
 
+	/**
+	 * Get the enclosing test classes of the current test or container.
+	 *
+	 * <p>This method is useful to look up annotations on nested test classes
+	 * and their enclosing <em>runtime</em> types:
+	 *
+	 * <pre>{@code
+	 * AnnotationSupport.findAnnotation(
+	 *     extensionContext.getRequiredTestClass(),
+	 *     MyAnnotation.class,
+	 *     extensionContext.getEnclosingTestClasses()
+	 * );
+	 * }</pre>
+	 *
+	 * @return an empty list if there is no class associated with the current
+	 * test or container or when it is not nested; otherwise, a list containing
+	 * the enclosing test classes in order from outermost to innermost; never
+	 * {@code null}
+	 *
+	 * @since 5.12.1
+	 * @see org.junit.platform.commons.support.AnnotationSupport#findAnnotation(Class, Class, List)
+	 */
+	@API(status = EXPERIMENTAL, since = "5.12.1")
+	List<Class<?>> getEnclosingTestClasses();
+
 	/**
 	 * Get the <em>required</em> {@link Class} associated with the current test
 	 * or container.
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java
index 546472781200..9350bfe4ea2d 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java
@@ -12,6 +12,7 @@
 
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.Optional;
 
 import org.junit.jupiter.api.TestInstance.Lifecycle;
@@ -54,6 +55,11 @@ public Optional<Class<?>> getTestClass() {
 		return Optional.of(getTestDescriptor().getTestClass());
 	}
 
+	@Override
+	public List<Class<?>> getEnclosingTestClasses() {
+		return getTestDescriptor().getEnclosingTestClasses();
+	}
+
 	@Override
 	public Optional<Lifecycle> getTestInstanceLifecycle() {
 		return Optional.of(this.lifecycle);
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java
index e4848c9351a0..d04824dd223b 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTemplateInvocationExtensionContext.java
@@ -12,6 +12,7 @@
 
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.Optional;
 
 import org.junit.jupiter.api.TestInstance.Lifecycle;
@@ -44,6 +45,11 @@ public Optional<Class<?>> getTestClass() {
 		return Optional.of(getTestDescriptor().getTestClass());
 	}
 
+	@Override
+	public List<Class<?>> getEnclosingTestClasses() {
+		return getTestDescriptor().getEnclosingTestClasses();
+	}
+
 	@Override
 	public Optional<Lifecycle> getTestInstanceLifecycle() {
 		return getParent().flatMap(ExtensionContext::getTestInstanceLifecycle);
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java
index cd3c292709e2..fc87ef3f3a68 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java
@@ -10,8 +10,11 @@
 
 package org.junit.jupiter.engine.descriptor;
 
+import static java.util.Collections.emptyList;
+
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.Optional;
 
 import org.junit.jupiter.api.TestInstance;
@@ -40,6 +43,11 @@ public Optional<Class<?>> getTestClass() {
 		return Optional.empty();
 	}
 
+	@Override
+	public List<Class<?>> getEnclosingTestClasses() {
+		return emptyList();
+	}
+
 	@Override
 	public Optional<TestInstance.Lifecycle> getTestInstanceLifecycle() {
 		return Optional.empty();
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java
index 6ecd845d6ded..66ba8a94c7ac 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java
@@ -10,8 +10,11 @@
 
 package org.junit.jupiter.engine.descriptor;
 
+import static java.util.Collections.emptyList;
+
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.Optional;
 
 import org.junit.jupiter.api.TestInstance.Lifecycle;
@@ -43,6 +46,11 @@ public Optional<Class<?>> getTestClass() {
 		return Optional.empty();
 	}
 
+	@Override
+	public List<Class<?>> getEnclosingTestClasses() {
+		return emptyList();
+	}
+
 	@Override
 	public Optional<Lifecycle> getTestInstanceLifecycle() {
 		return Optional.empty();
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java
index c38ecd72fd77..7fc96c4cf07d 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java
@@ -50,7 +50,8 @@
  * @since 5.0
  */
 @API(status = INTERNAL, since = "5.0")
-public abstract class MethodBasedTestDescriptor extends JupiterTestDescriptor implements ResourceLockAware {
+public abstract class MethodBasedTestDescriptor extends JupiterTestDescriptor
+		implements ResourceLockAware, TestClassAware {
 
 	private static final Logger logger = LoggerFactory.getLogger(MethodBasedTestDescriptor.class);
 
@@ -106,7 +107,8 @@ public Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> getResou
 				getTestMethod()));
 	}
 
-	private List<Class<?>> getEnclosingTestClasses() {
+	@Override
+	public List<Class<?>> getEnclosingTestClasses() {
 		return getParent() //
 				.filter(TestClassAware.class::isInstance) //
 				.map(TestClassAware.class::cast) //
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java
index a6d983e7196b..24cdd6914eb3 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java
@@ -12,6 +12,7 @@
 
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.Optional;
 
 import org.junit.jupiter.api.TestInstance.Lifecycle;
@@ -51,6 +52,11 @@ public Optional<Class<?>> getTestClass() {
 		return Optional.of(getTestDescriptor().getTestClass());
 	}
 
+	@Override
+	public List<Class<?>> getEnclosingTestClasses() {
+		return getTestDescriptor().getEnclosingTestClasses();
+	}
+
 	@Override
 	public Optional<Lifecycle> getTestInstanceLifecycle() {
 		return getParent().flatMap(ExtensionContext::getTestInstanceLifecycle);
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java
index ea6df34d7436..5fa74917714b 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java
@@ -12,6 +12,7 @@
 
 import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Method;
+import java.util.List;
 import java.util.Optional;
 
 import org.junit.jupiter.api.TestInstance.Lifecycle;
@@ -47,6 +48,11 @@ public Optional<Class<?>> getTestClass() {
 		return Optional.of(getTestDescriptor().getTestClass());
 	}
 
+	@Override
+	public List<Class<?>> getEnclosingTestClasses() {
+		return getTestDescriptor().getEnclosingTestClasses();
+	}
+
 	@Override
 	public Optional<Lifecycle> getTestInstanceLifecycle() {
 		return getParent().flatMap(ExtensionContext::getTestInstanceLifecycle);
diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java
index ece10478da6e..1f30697db788 100644
--- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java
@@ -165,7 +165,7 @@ public static <A extends Annotation> Optional<A> findAnnotation(AnnotatedElement
 	 * @since 1.8
 	 * @see SearchOption
 	 * @see #findAnnotation(AnnotatedElement, Class)
-	 * @deprecated Use {@link #findAnnotation(Class, Class, List)}
+	 * @deprecated Use {@link #findAnnotation(AnnotatedElement, Class)}
 	 * (for {@code SearchOption.DEFAULT}) or
 	 * {@link #findAnnotation(Class, Class, List)} (for
 	 * {@code SearchOption.INCLUDE_ENCLOSING_CLASSES}) instead


Test patch:
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java
index 11b50f5dfd2b..ee9716201092 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java
@@ -40,6 +40,7 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayNameGenerator;
 import org.junit.jupiter.api.Named;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.Extension;
@@ -112,33 +113,63 @@ void fromJupiterEngineDescriptor() {
 	}
 
 	@Test
-	@SuppressWarnings("resource")
 	void fromClassTestDescriptor() {
 		var nestedClassDescriptor = nestedClassDescriptor();
 		var outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor);
+		var doublyNestedClassDescriptor = doublyNestedClassDescriptor();
+		var methodTestDescriptor = nestedMethodDescriptor();
+		nestedClassDescriptor.addChild(doublyNestedClassDescriptor);
+		nestedClassDescriptor.addChild(methodTestDescriptor);
 
 		var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD,
 			configuration, extensionRegistry, null);
 
 		// @formatter:off
 		assertAll("outerContext",
-			() -> assertThat(outerExtensionContext.getElement()).contains(OuterClass.class),
-			() -> assertThat(outerExtensionContext.getTestClass()).contains(OuterClass.class),
+			() -> assertThat(outerExtensionContext.getElement()).contains(OuterClassTestCase.class),
+			() -> assertThat(outerExtensionContext.getTestClass()).contains(OuterClassTestCase.class),
 			() -> assertThat(outerExtensionContext.getTestInstance()).isEmpty(),
 			() -> assertThat(outerExtensionContext.getTestMethod()).isEmpty(),
-			() -> assertThat(outerExtensionContext.getRequiredTestClass()).isEqualTo(OuterClass.class),
+			() -> assertThat(outerExtensionContext.getRequiredTestClass()).isEqualTo(OuterClassTestCase.class),
 			() -> assertThrows(PreconditionViolationException.class, outerExtensionContext::getRequiredTestInstance),
 			() -> assertThrows(PreconditionViolationException.class, outerExtensionContext::getRequiredTestMethod),
 			() -> assertThat(outerExtensionContext.getDisplayName()).isEqualTo(outerClassDescriptor.getDisplayName()),
 			() -> assertThat(outerExtensionContext.getParent()).isEmpty(),
 			() -> assertThat(outerExtensionContext.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD),
-			() -> assertThat(outerExtensionContext.getExtensions(PreInterruptCallback.class)).isEmpty()
+			() -> assertThat(outerExtensionContext.getExtensions(PreInterruptCallback.class)).isEmpty(),
+			() -> assertThat(outerExtensionContext.getEnclosingTestClasses()).isEmpty()
 		);
 		// @formatter:on
 
 		var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor,
 			PER_METHOD, configuration, extensionRegistry, null);
-		assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext);
+		// @formatter:off
+		assertAll("nestedContext",
+			() -> assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext),
+			() -> assertThat(nestedExtensionContext.getTestClass()).contains(OuterClassTestCase.NestedClass.class),
+			() -> assertThat(nestedExtensionContext.getEnclosingTestClasses()).containsExactly(OuterClassTestCase.class)
+		);
+		// @formatter:on
+
+		var doublyNestedExtensionContext = new ClassExtensionContext(nestedExtensionContext, null,
+			doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, null);
+		// @formatter:off
+		assertAll("doublyNestedContext",
+				() -> assertThat(doublyNestedExtensionContext.getParent()).containsSame(nestedExtensionContext),
+				() -> assertThat(doublyNestedExtensionContext.getTestClass()).contains(OuterClassTestCase.NestedClass.DoublyNestedClass.class),
+				() -> assertThat(doublyNestedExtensionContext.getEnclosingTestClasses()).containsExactly(OuterClassTestCase.class, OuterClassTestCase.NestedClass.class)
+		);
+		// @formatter:on
+
+		var methodExtensionContext = new MethodExtensionContext(nestedExtensionContext, null, methodTestDescriptor,
+			configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector());
+		// @formatter:off
+		assertAll("methodContext",
+				() -> assertThat(methodExtensionContext.getParent()).containsSame(nestedExtensionContext),
+				() -> assertThat(methodExtensionContext.getTestClass()).contains(OuterClassTestCase.NestedClass.class),
+				() -> assertThat(methodExtensionContext.getEnclosingTestClasses()).containsExactly(OuterClassTestCase.class)
+		);
+		// @formatter:on
 	}
 
 	@Test
@@ -155,7 +186,6 @@ void ExtensionContext_With_ExtensionRegistry_getExtensions() {
 	}
 
 	@Test
-	@SuppressWarnings("resource")
 	void tagsCanBeRetrievedInExtensionContext() {
 		var nestedClassDescriptor = nestedClassDescriptor();
 		var outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor);
@@ -175,20 +205,19 @@ void tagsCanBeRetrievedInExtensionContext() {
 
 		var methodExtensionContext = new MethodExtensionContext(outerExtensionContext, null, methodTestDescriptor,
 			configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector());
-		methodExtensionContext.setTestInstances(DefaultTestInstances.of(new OuterClass()));
+		methodExtensionContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase()));
 		assertThat(methodExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "method-tag");
 		assertThat(methodExtensionContext.getRoot()).isSameAs(outerExtensionContext);
 	}
 
 	@Test
-	@SuppressWarnings("resource")
 	void fromMethodTestDescriptor() {
 		var methodTestDescriptor = methodDescriptor();
 		var classTestDescriptor = outerClassDescriptor(methodTestDescriptor);
 		var engineDescriptor = new JupiterEngineDescriptor(UniqueId.forEngine("junit-jupiter"), configuration);
 		engineDescriptor.addChild(classTestDescriptor);
 
-		Object testInstance = new OuterClass();
+		Object testInstance = new OuterClassTestCase();
 		var testMethod = methodTestDescriptor.getTestMethod();
 
 		var engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor, configuration,
@@ -202,10 +231,11 @@ void fromMethodTestDescriptor() {
 		// @formatter:off
 		assertAll("methodContext",
 			() -> assertThat(methodExtensionContext.getElement()).contains(testMethod),
-			() -> assertThat(methodExtensionContext.getTestClass()).contains(OuterClass.class),
+			() -> assertThat(methodExtensionContext.getTestClass()).contains(OuterClassTestCase.class),
+			() -> assertThat(methodExtensionContext.getEnclosingTestClasses()).isEmpty(),
 			() -> assertThat(methodExtensionContext.getTestInstance()).contains(testInstance),
 			() -> assertThat(methodExtensionContext.getTestMethod()).contains(testMethod),
-			() -> assertThat(methodExtensionContext.getRequiredTestClass()).isEqualTo(OuterClass.class),
+			() -> assertThat(methodExtensionContext.getRequiredTestClass()).isEqualTo(OuterClassTestCase.class),
 			() -> assertThat(methodExtensionContext.getRequiredTestInstance()).isEqualTo(testInstance),
 			() -> assertThat(methodExtensionContext.getRequiredTestMethod()).isEqualTo(testMethod),
 			() -> assertThat(methodExtensionContext.getDisplayName()).isEqualTo(methodTestDescriptor.getDisplayName()),
@@ -360,7 +390,7 @@ void usingStore() {
 			configuration, extensionRegistry, null);
 		var childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, configuration,
 			extensionRegistry, new OpenTest4JAwareThrowableCollector());
-		childContext.setTestInstances(DefaultTestInstances.of(new OuterClass()));
+		childContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase()));
 
 		var childStore = childContext.getStore(Namespace.GLOBAL);
 		var parentStore = parentContext.getStore(Namespace.GLOBAL);
@@ -431,13 +461,18 @@ void configurationParameter(Function<JupiterConfiguration, ? extends ExtensionCo
 	}
 
 	private NestedClassTestDescriptor nestedClassDescriptor() {
-		return new NestedClassTestDescriptor(UniqueId.root("nested-class", "NestedClass"), OuterClass.NestedClass.class,
-			List::of, configuration);
+		return new NestedClassTestDescriptor(UniqueId.root("nested-class", "NestedClass"),
+			OuterClassTestCase.NestedClass.class, List::of, configuration);
+	}
+
+	private NestedClassTestDescriptor doublyNestedClassDescriptor() {
+		return new NestedClassTestDescriptor(UniqueId.root("nested-class", "DoublyNestedClass"),
+			OuterClassTestCase.NestedClass.DoublyNestedClass.class, List::of, configuration);
 	}
 
 	private ClassTestDescriptor outerClassDescriptor(TestDescriptor child) {
-		var classTestDescriptor = new ClassTestDescriptor(UniqueId.root("class", "OuterClass"), OuterClass.class,
-			configuration);
+		var classTestDescriptor = new ClassTestDescriptor(UniqueId.root("class", "OuterClass"),
+			OuterClassTestCase.class, configuration);
 		if (child != null) {
 			classTestDescriptor.addChild(child);
 		}
@@ -446,19 +481,41 @@ private ClassTestDescriptor outerClassDescriptor(TestDescriptor child) {
 
 	private TestMethodTestDescriptor methodDescriptor() {
 		try {
-			return new TestMethodTestDescriptor(UniqueId.root("method", "aMethod"), OuterClass.class,
-				OuterClass.class.getDeclaredMethod("aMethod"), List::of, configuration);
+			return new TestMethodTestDescriptor(UniqueId.root("method", "aMethod"), OuterClassTestCase.class,
+				OuterClassTestCase.class.getDeclaredMethod("aMethod"), List::of, configuration);
 		}
 		catch (NoSuchMethodException e) {
 			throw new RuntimeException(e);
 		}
 	}
 
+	private TestMethodTestDescriptor nestedMethodDescriptor() {
+		try {
+			return new TestMethodTestDescriptor(UniqueId.root("method", "nestedMethod"),
+				OuterClassTestCase.NestedClass.class, BaseNestedTestCase.class.getDeclaredMethod("nestedMethod"),
+				List::of, configuration);
+		}
+		catch (NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	static abstract class BaseNestedTestCase {
+		@Test
+		void nestedMethod() {
+		}
+
+		@Nested
+		class DoublyNestedClass {
+		}
+	}
+
 	@Tag("outer-tag")
-	static class OuterClass {
+	static class OuterClassTestCase {
 
 		@Tag("nested-tag")
-		static class NestedClass {
+		@Nested
+		class NestedClass extends BaseNestedTestCase {
 		}
 
 		@Tag("method-tag")
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java
index 5550270a5300..65aeef93efd9 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java
@@ -23,6 +23,7 @@
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -256,6 +257,11 @@ public Optional<Class<?>> getTestClass() {
 				return Optional.empty();
 			}
 
+			@Override
+			public List<Class<?>> getEnclosingTestClasses() {
+				return List.of();
+			}
+
 			@Override
 			public Optional<Lifecycle> getTestInstanceLifecycle() {
 				return Optional.empty();