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

Issue:
Introduce extension API for accessing arguments of parameterized tests
## Issue

Extension methods all have `ExtensionContext` which provides us many useful information like `displayname`, `method` or `tags`. This helps  in writing custom test reports.

What I'm missing is a way to get argument instances of a `@ParameterizedTest`. When not running tests extended by a `ParameterResolver` using `ExtensionContext.Store` you can only make a workaround to get a clue of what arguments were used for the test:

- Filter displayname (objects need to override `toString()` for important informations)
- get values from annotations

If parameters are simple strings, integers etc. there is no problem when parsing displayname. The problem starts when one of the parameters is an object containing lots of informations. A new instance would have to be created when analyzing the test, but this doesn't garanty an object with exact the same content like in the test and might also lead to memory issues.

Most simple way is to reimplement Parameterized Test Template, extend from each class, catch resolved parameters and store them directly in ExtensionContext.Store, but I'm not a fan of writing Wrapper classes for such small extensions in existing codes.

## Possible Suggestions

- Provide a method `Arguments[] ExtensionContext.getArguments()` or `List<Arguments> ExtensionContext.getArguments()` which returns a list of all Arguments used for the test (empty list if no arguments were used).
- Store arguments in `ExtensionContext.Store` with a predefined namespace in your `ParameterizedTestParameterResolver` class (then there is no need to do it in custom `ArgumentsProvider` classes)
- Provide an Extension interface which is called between `BeforeTestExecutionCallback` and `@Test`. As parameters it should have `ExtensionContext` and `Arguments[]`.

## Related Issues

- #944
- #1668
- #1884



Discussion / hints:
This might be useful for more than just `@ParameterizedTest` methods. Right now the arguments for a test method get resolved immediately before calling it, i.e. after `BeforeTestExecutionCallback` extensions are called. When would you need the arguments? Would it suffice to get access to them after the test has been executed?
Test analysis are mostly done after the test execution, so I think it's ok to just have them in one of the AfterXXX interfaces. Getting arguments before test execution would also be nice, but instead of storing them in an attribute I don't know what someone should do with them before the actual test.

I use an annotation (similar to @ValuesSource) which accepts a String[] of filenames containing data for data-driven tests. An ArgumentsProvider checks if "filename" exists and stores information in an object. The object stores quite a lot of information which can't be displayed in a single String, so getDisplayname() to fetch informations is not an option.

Priority can be set to medium, it is a nice-to-have feature with lots of advantages on test reporting side. If you are able to release JUnit 5.1 before March 2018 I'm more than satisfied.

I think introducing a new extension point that is called between `BeforeTestExecutionCallback` and `@Test` would offer the most flexibility.

It could look like this:

```java
interface ArgumentsProcessor extends Extension {
    void processTestMethodArguments(ExtensionContext context, Object[] arguments);
}
```

I guess it should only be called when `arguments` is not empty, i.e. the test method has parameters. Changing the array would cause the test method to be called with changed arguments.

Lifecycle methods and test class constructors can also have arguments that get resolved by `ParameterResolvers`. Since I cannot think of a possible use case for those, I wouldn't call extensions. We could always add additional methods like `processConstructorArguments(...)` later.

@junit-team/junit-lambda Thoughts?
I think that's a reasonable idea.

> I guess it should only be called when arguments is not empty, i.e. the test method has parameters. 

That would make sense.

> Changing the array would cause the test method to be called with changed arguments.

That seems potentially risky. I'll have to ponder that a bit. 😉 
> Since I cannot think of a possible use case for those, I wouldn't call extensions. We could always add additional methods like processConstructorArguments(...) later.

If we foresee that, we should either change the name of the `ArgumentsProcessor` API to reflect that or introduce all such methods as no-op `default` methods.
Will this extension also be called when method just contains `TestInfo` or `TestReporter`? According to previous posts I can imagine that this might happen and if it would be critical for test environment.
Yes, it would be called for any arguments. If you're not interested in `TestInfo` or `TestReporter`, you could not report them in your extension implementation or would that be a problem?
You can easily ignore these arguments just by using "instanceof TestInfo" or "instanceof TestReporter", no need to create special cases because of that.
> > Changing the array would cause the test method to be called with changed arguments.

> That seems potentially risky. I'll have to ponder that a bit. :wink:

I have a suggestion for this issue: why not calling the interface between `@Test` and `AfterTestExecutionCallback` (doesn't matter if before or after `TestExecutionExceptionHandler`). Then you would avoid a possible change of arguments before the actual test execution
The Eclipse Jetty project would like to have this ability.  
To be able to write an Extension that has a fully resolved TestInfo (with parameters) just before the test is actually called.
Marking this issue "up-for-grabs". @joakime / @olamy want to give it a shot?

Implementation outline as @marcphilipp described above in https://github.com/junit-team/junit5/issues/1139#issuecomment-341683075
I've put more thought into this and now have additional feedback.

> I think introducing a new extension point that is called between `BeforeTestExecutionCallback` and `@Test` would offer the most flexibility.

I agree.

> It could look like this:
> 
> ```java
> interface ArgumentsProcessor extends Extension {
>     void processTestMethodArguments(ExtensionContext context, Object[] arguments);
> }
> ```

I think the signature is fine, but I'm not yet convinced regarding the naming. See following points.

> I guess it should only be called when `arguments` is not empty, i.e. the test method has parameters.

I think we might need to always invoke the extension even if the array is empty. Otherwise, extensions that implement the API might have to _guess_ why this API was not honored for particular scenarios.

> Changing the array would cause the test method to be called with changed arguments.

I don't think that is a good idea. As far as I can tell, nobody has requested to be able to _change_ the arguments provided to a test method. Thus, I would rather pass a copy of the arguments array to such an extension.

If the community clamors for such support, we could always change that aspect at a later date.

This leads to me wonder what the best name for such an extension API would be.

What would such an extension implementation actually _do_ with the arguments?

Maybe "processing" is generic enough, but I'm wondering if there's something better out there. 😉 

> Lifecycle methods and test class constructors can also have arguments that get resolved by `ParameterResolvers`. Since I cannot think of a possible use case for those, I wouldn't call extensions. We could always add additional methods like `processConstructorArguments(...)` later.

That's true; however, we'll have to introduce a flag for methods in our `ExecutableInvoker` that specifies whether or not the method being invoked is a test method, and only if the flag is `true` would we want to invoke the extension API proposed here.

The passed in arguments might be useful for some, but for Eclipse Jetty, just having `ExtensionContext.getDisplayName()` be valid/sane would be sufficient.
Similar to what `TestInfo.getDisplayName()` shows while in the test itself.

> just having `ExtensionContext.getDisplayName()` be valid/sane would be sufficient.

When does that return something "invalid" for you?

> Similar to what `TestInfo.getDisplayName()` shows while in the test itself.

The display name returned by `ExtensionContext` and `TestInfo` must actually be the same `String`, unmodified.




If it's ok, I'd like to submit a WIP pull request for this after looking at it in relation to an extension I'm writing...

I wasn't too keen on the naming as ArgumentsProcessor either so set it up as follows for now:

```
public interface BeforeParameterizedTestExecutionCallback extends Extension {
	void beforeParameterizedTestExecution(ExtensionContext context, Object[] arguments);
}
```

On a related note, I also have a version of this that puts the parameter array into the extension's Store with method scope.  Here, any existing callback can get to the parameters if required.  With this method, a new callback obviously wouldn't be required.

We talked about this issue in our team call today and agreed that we need to put some more thought into how the extension/API should look before continuing with the PR.
An update on this would be great. Use case is custom reporting application where I want to aggregate based on the parameterized tests name and also pass the param.

The workaround is to have a pattern for how parametrized tests display names and then parsing it which is definitely suboptimal
@foxfortune
I abandoned this and wrote a quick bit of reflection to get the test parameters in the extension. Sure, it's brittle but works for me at the moment (on 5.5.2).

I call the following from `beforeTestExecution`.  It returns an empty array if the test isn't parameterised : 

```java
private Object[] argumentsFrom(ExtensionContext context) {
        try {
            TestMethodTestDescriptor testDescriptor = invokeMethod(context, "getTestDescriptor", TestMethodTestDescriptor.class);
            TestTemplateInvocationContext invocationContext = fieldValue(testDescriptor, "invocationContext", TestTemplateInvocationContext.class);
            return fieldValue(invocationContext, "arguments", Object[].class);
        } catch (Exception e) {
            return new Object[0];
        }
    }
```

You'll need to replace `invokeMethod` and `fieldValue` with calls to your preferred reflection lib (or use the one built in to JUnit5 if you like it).

I'd prefer it if the parameters (if any) were just made available in the ExtensionContext subclass passed to the existing callback methods.  I don't really see the point of having a new specific callback method as it doesn't feel like a lifecycle callback and not really where I want access to the parameters.
@paul-brooks 

Thanks for the pointer, what library reflection lib are you using? I don't really have a preference and am floundering trying to implement the JUnit5 built in one.

Also not sure I am reading the invokeMethod properly, however I dont see a getTestDescriptor method in ExtensionContext class which is where I think I am having trouble.

Also agree I would love the params to just be in the ExtensionContext
I spoke too soon, I was able to finally get it to work! I used the JUnit 5 reflection in case anyone else would like to use it, here is the code, which I am sure could also be tightened up:

```
try {
            Method method = ReflectionUtils.findMethod(context.getClass(),"getTestDescriptor").orElse(null);
            TestMethodTestDescriptor descriptor = (TestMethodTestDescriptor) ReflectionUtils.invokeMethod(method, context);

            //Get the TestTemplateInvocationContext
            Field templateField = descriptor.getClass().getDeclaredField("invocationContext");
            templateField.setAccessible(true);
            TestTemplateInvocationContext template = (TestTemplateInvocationContext) templateField.get(descriptor);

            //Get the params finally
            Field argumentsField = template.getClass().getDeclaredField("arguments");
            argumentsField.setAccessible(true);
            Object[] params = (Object[]) argumentsField.get(template);

            return params;

        }catch(Exception e)
        {
            return new Object[0];
        }
```
@foxfortune 

Glad you got it working.  My extension has it's own little reflection util as I find it easier to work with.
@foxfortune  and @paul-brooks ,thanks for sharing first of all. The code works fine for getting the argument values; however, how could we obtain the argument names?
btw, @marcphilipp I also think that we need a clean way to obtain parameters and their names so external tools can eventually process them for reporting purposes

> @foxfortune and @paul-brooks ,thanks for sharing first of all. The code works fine for getting the argument values; however, how could we obtain the argument names?
> btw, @marcphilipp I also think that we need a clean way to obtain parameters and their names so external tools can eventually process them for reporting purposes

@bitcoder You can now use the `InvocationInterceptor` to obtain the parameter value array more easily, without using (your own) reflection, for example in Kotlin:

```kotlin
    // Called for @ParameterizedTest methods
    override fun interceptTestTemplateMethod(
        invocation: InvocationInterceptor.Invocation<Void>,
        invocationContext: ReflectiveInvocationContext<Method>,
        context: ExtensionContext
    ) {
        doSomethingWith(context, invocationContext.arguments.toTypedArray())
        invocation.proceed()
    }

    // Called for @Test methods
    override fun interceptTestMethod(
        invocation: InvocationInterceptor.Invocation<Void>,
        invocationContext: ReflectiveInvocationContext<Method>,
        context: ExtensionContext
    ) {
        doSomethingWith(context, invocationContext.arguments.toTypedArray())
        invocation.proceed()
    }
```

As for parameter names, I think this is only possible via reflection if you have compiled with `-g` to include debug information.

For my extension, I'm parsing the source files with Antlr so have access to the parameter names that way.
Just to add another use case, it would be helpful to also make the parameterized invocation information available to the methods implemented for @EnabledIf / @DisabledIf, so that I can skip specific indexes when running a parameterized test.

Would the `InvocationInterceptor` pattern also work there?
> As for parameter names, I think this is only possible via reflection if you have compiled with `-g` to include debug information.

`-g` will include debug symbols in the .class files, which can be retrieved via libraries such as ASM that parse the actual bytecode, but that's more challenging than using the built-in `java.lang.reflect.Parameter` reflection APIs.

To obtain parameter names via the `Parameter` API, the `-parameters` flag must be supplied to the Java compiler.
> Just to add another use case, it would be helpful to also make the parameterized invocation information available to the methods implemented for @EnabledIf / @DisabledIf, so that I can skip specific indexes when running a parameterized test.
> 
> Would the `InvocationInterceptor` pattern also work there?

There is a [pending PR](https://github.com/junit-pioneer/junit-pioneer/pull/368) in [JUnit Pioneer](https://junit-pioneer.org) which basically does this. From the [PR's docs](https://github.com/junit-pioneer/junit-pioneer/blob/b90527495c9f8281ee88fb5bf21eb2196c94e9f7/docs/disable-parameterized-tests.adoc#disableifparameter):

>This extension can be used to selectively disable parameterized tests based on their parameter values (converted with `toString()`). The extension comes with three annotations, covering different use-cases:
>
>- `@DisableIfAnyParameter`, non-repeatable
>- `@DisableIfAllParameters`, non-repeatable
>- `@DisableIfParameter`, repeatable

The underyling [`DisableIfParameterExtension`](https://github.com/junit-pioneer/junit-pioneer/blob/b90527495c9f8281ee88fb5bf21eb2196c94e9f7/src/main/java/org/junitpioneer/jupiter/params/DisableIfParameterExtension.java) implements `InvocationInterceptor`.
This issue has been automatically marked as stale because it has not had recent activity. Given the limited bandwidth of the team, it will be automatically closed if no further activity occurs. Thank you for your contribution.

This issue has been automatically closed due to inactivity. If you have a good use case for this feature, please feel free to reopen the issue.

I have a case:
I would like to implement an extension based on `ExecutionCondition` in which I would check if a test (case) from `ParameterizedTest` which has a parameter `targetTestEnv` should be run for the current test environment.

Sample code
```java
@ParameterizedTest
@CsvSource(textBlock = """
           TC1,    targetTestEnv1,    testData1ForTestEnv1
           TC2,    targetTestEnv2,    testData1ForTestEnv2
""")
void test(String tcId, String targetTestEnv, String testData) {
    ...
}
```

```java
public class AutoTargetTestEnvCheck implements ExecutionCondition {

    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
        String testEnv = System.getProperty("testEnv");        
        String testCaseTargetEnv = <need a way to get it>

        ...

        if (testCaseTargetEnv .equalsIgnoreCase(testEnv)) {
            return ConditionEvaluationResult.enabled("Current test env matches the target test env of the test case.");
        }

        return ConditionEvaluationResult
                .disabled("Current test env DOES NOT match the target test env of the test case.");
    }

}
```

---
BTW, the [Disable Parameterized Test](https://junit-pioneer.org/docs/disable-parameterized-tests/) extensions from [JUnit Pioneer](https://junit-pioneer.org/) mentioned by @beatngu13 do not work for my case. Because the filtering value i.e. testEnv will be provided through annotation attribute which must be a constant expression.

For example:
```java
class MyTestClass {

    static final String testEnv = System.getProperty("testEnv");

    @DisableIfAnyArgument(contains = testEnv)
    @ParameterizedTest
    @CsvSource(textBlock = """
                    TC1,    targetTestEnv1,    testDataForEnv1,
                    TC2,    targetTestEnv2,    testDataForEnv2,
            """)
    void myTest(String tcId, String targetTestEnv, String testData) {
        ....
    }
}
```

The line `@DisableIfAnyArgument(contains = testEnv )` will throw error
`The value for annotation attribute DisableIfAnyArgument.contains must be a constant expression`
@TimeInvestor Your use case can be solved by implementing a custom extension rather than reusing the one from junit-pioneer:

```java
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ExtendWith(AbortIfTestEnv.Extension.class)
public @interface AbortIfTestEnv {

	class Extension implements InvocationInterceptor {

		@Override
		public void interceptTestTemplateMethod(Invocation<Void> invocation,
				ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext)
				throws Throwable {
			String testEnv = System.getProperty("testEnv");
			Stream<String> arguments = invocationContext.getArguments().stream().map(Object::toString);
			assumeTrue(
					arguments.noneMatch(it -> it.contains(testEnv)),
					"Skipping test because an argument contains the test environment"
			);
			invocation.proceed();
		}
	}
}
```
We should revisit this after #878 is resolved because parameterization on the class level will support both constructor and field injection and therefore require a different solution. One idea would be to store an `ArgumentAccessor` in `ExtensionContext.Store` and document the namespace and key so that other extensions can read it.

Fix patch:
diff --git a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java
index 38bd5694b3d6..592be9f94445 100644
--- a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java
+++ b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java
@@ -50,7 +50,7 @@ public void formatTestNames(Blackhole blackhole) throws Exception {
 		var method = TestCase.class.getDeclaredMethod("parameterizedTest", int.class);
 		var formatter = new ParameterizedInvocationNameFormatter(
 			DISPLAY_NAME_PLACEHOLDER + " " + DEFAULT_DISPLAY_NAME + " ({0})", "displayName",
-			new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class)), 512);
+			new ParameterizedTestContext(TestCase.class, method, method.getAnnotation(ParameterizedTest.class)), 512);
 		for (int i = 0; i < argumentsList.size(); i++) {
 			Arguments arguments = argumentsList.get(i);
 			blackhole.consume(formatter.format(i, EvaluatedArgumentSet.allOf(arguments)));
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java
new file mode 100644
index 000000000000..96fffa5039d5
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/DefaultParameterInfo.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.jupiter.params;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
+import org.junit.jupiter.params.support.ParameterDeclarations;
+import org.junit.jupiter.params.support.ParameterInfo;
+
+/**
+ * @since 5.13
+ */
+class DefaultParameterInfo implements ParameterInfo {
+
+	private final ParameterDeclarations declarations;
+	private final ArgumentsAccessor arguments;
+
+	DefaultParameterInfo(ParameterDeclarations declarations, ArgumentsAccessor arguments) {
+		this.declarations = declarations;
+		this.arguments = arguments;
+	}
+
+	@Override
+	public ParameterDeclarations getDeclarations() {
+		return this.declarations;
+	}
+
+	@Override
+	public ArgumentsAccessor getArguments() {
+		return this.arguments;
+	}
+
+	void store(ExtensionContext context) {
+		context.getStore(NAMESPACE).put(KEY, this);
+	}
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java
index faa837b9918e..31528072b80a 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java
@@ -40,7 +40,7 @@
 
 class ParameterizedClassContext implements ParameterizedDeclarationContext<ClassTemplateInvocationContext> {
 
-	private final Class<?> clazz;
+	private final Class<?> testClass;
 	private final ParameterizedClass annotation;
 	private final TestInstance.Lifecycle testInstanceLifecycle;
 	private final ResolverFacade resolverFacade;
@@ -48,28 +48,28 @@ class ParameterizedClassContext implements ParameterizedDeclarationContext<Class
 	private final List<ArgumentSetLifecycleMethod> beforeMethods;
 	private final List<ArgumentSetLifecycleMethod> afterMethods;
 
-	ParameterizedClassContext(Class<?> clazz, ParameterizedClass annotation,
+	ParameterizedClassContext(Class<?> testClass, ParameterizedClass annotation,
 			TestInstance.Lifecycle testInstanceLifecycle) {
-		this.clazz = clazz;
+		this.testClass = testClass;
 		this.annotation = annotation;
 		this.testInstanceLifecycle = testInstanceLifecycle;
 
-		List<Field> fields = findParameterAnnotatedFields(clazz);
+		List<Field> fields = findParameterAnnotatedFields(testClass);
 		if (fields.isEmpty()) {
-			this.resolverFacade = ResolverFacade.create(ReflectionUtils.getDeclaredConstructor(clazz), annotation);
+			this.resolverFacade = ResolverFacade.create(ReflectionUtils.getDeclaredConstructor(testClass), annotation);
 			this.injectionType = InjectionType.CONSTRUCTOR;
 		}
 		else {
-			this.resolverFacade = ResolverFacade.create(clazz, fields);
+			this.resolverFacade = ResolverFacade.create(testClass, fields);
 			this.injectionType = InjectionType.FIELDS;
 		}
 
-		this.beforeMethods = findLifecycleMethodsAndAssertStaticAndNonPrivate(clazz, testInstanceLifecycle, TOP_DOWN,
-			BeforeParameterizedClassInvocation.class, BeforeParameterizedClassInvocation::injectArguments,
+		this.beforeMethods = findLifecycleMethodsAndAssertStaticAndNonPrivate(testClass, testInstanceLifecycle,
+			TOP_DOWN, BeforeParameterizedClassInvocation.class, BeforeParameterizedClassInvocation::injectArguments,
 			this.resolverFacade);
 
 		// Make a local copy since findAnnotatedMethods() returns an immutable list.
-		this.afterMethods = new ArrayList<>(findLifecycleMethodsAndAssertStaticAndNonPrivate(clazz,
+		this.afterMethods = new ArrayList<>(findLifecycleMethodsAndAssertStaticAndNonPrivate(testClass,
 			testInstanceLifecycle, BOTTOM_UP, AfterParameterizedClassInvocation.class,
 			AfterParameterizedClassInvocation::injectArguments, this.resolverFacade));
 
@@ -87,6 +87,11 @@ private static List<Field> findParameterAnnotatedFields(Class<?> clazz) {
 		return findFields(clazz, it -> isAnnotated(it, Parameter.class), BOTTOM_UP);
 	}
 
+	@Override
+	public Class<?> getTestClass() {
+		return this.testClass;
+	}
+
 	@Override
 	public ParameterizedClass getAnnotation() {
 		return this.annotation;
@@ -94,7 +99,7 @@ public ParameterizedClass getAnnotation() {
 
 	@Override
 	public Class<?> getAnnotatedElement() {
-		return this.clazz;
+		return this.testClass;
 	}
 
 	@Override
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java
index aa532646db6f..11dad62adc72 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java
@@ -20,6 +20,8 @@
  */
 interface ParameterizedDeclarationContext<C> {
 
+	Class<?> getTestClass();
+
 	Annotation getAnnotation();
 
 	AnnotatedElement getAnnotatedElement();
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java
index 88285f29f9f6..207817c69ebb 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java
@@ -10,12 +10,17 @@
 
 package org.junit.jupiter.params;
 
+import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader;
+
 import java.util.Arrays;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
+import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor;
 import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.support.ParameterDeclarations;
 
 class ParameterizedInvocationContext<T extends ParameterizedDeclarationContext<?>> {
 
@@ -44,7 +49,8 @@ public void prepareInvocation(ExtensionContext context) {
 		if (this.declarationContext.isAutoClosingArguments()) {
 			registerAutoCloseableArgumentsInStoreForClosing(context);
 		}
-		new ArgumentCountValidator(this.declarationContext, this.arguments).validate(context);
+		validateArgumentCount(context);
+		storeParameterInfo(context);
 	}
 
 	private void registerAutoCloseableArgumentsInStoreForClosing(ExtensionContext context) {
@@ -58,6 +64,18 @@ private void registerAutoCloseableArgumentsInStoreForClosing(ExtensionContext co
 				.forEach(closeable -> store.put(argumentIndex.incrementAndGet(), closeable));
 	}
 
+	private void validateArgumentCount(ExtensionContext context) {
+		new ArgumentCountValidator(this.declarationContext, this.arguments).validate(context);
+	}
+
+	private void storeParameterInfo(ExtensionContext context) {
+		ParameterDeclarations declarations = this.declarationContext.getResolverFacade().getIndexedParameterDeclarations();
+		ClassLoader classLoader = getClassLoader(this.declarationContext.getTestClass());
+		Object[] arguments = this.arguments.getConsumedPayloads();
+		ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments);
+		new DefaultParameterInfo(declarations, accessor).store(context);
+	}
+
 	private static class CloseableArgument implements ExtensionContext.Store.CloseableResource {
 
 		private final AutoCloseable autoCloseable;
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java
index b716d84543a9..50a9417b3a11 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java
@@ -24,16 +24,23 @@
  */
 class ParameterizedTestContext implements ParameterizedDeclarationContext<TestTemplateInvocationContext> {
 
+	private final Class<?> testClass;
 	private final Method method;
 	private final ParameterizedTest annotation;
 	private final ResolverFacade resolverFacade;
 
-	ParameterizedTestContext(Method method, ParameterizedTest annotation) {
+	ParameterizedTestContext(Class<?> testClass, Method method, ParameterizedTest annotation) {
+		this.testClass = testClass;
 		this.method = Preconditions.notNull(method, "method must not be null");
 		this.annotation = Preconditions.notNull(annotation, "annotation must not be null");
 		this.resolverFacade = ResolverFacade.create(method, annotation);
 	}
 
+	@Override
+	public Class<?> getTestClass() {
+		return this.testClass;
+	}
+
 	@Override
 	public ParameterizedTest getAnnotation() {
 		return this.annotation;
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java
index 56b29245dec0..5097d9f02322 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java
@@ -35,8 +35,8 @@ public boolean supportsTestTemplate(ExtensionContext context) {
 			return false;
 		}
 
-		ParameterizedTestContext methodContext = new ParameterizedTestContext(context.getRequiredTestMethod(),
-			annotation.get());
+		ParameterizedTestContext methodContext = new ParameterizedTestContext(context.getRequiredTestClass(),
+			context.getRequiredTestMethod(), annotation.get());
 
 		getStore(context).put(DECLARATION_CONTEXT_KEY, methodContext);
 
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java
index 7ea70fadf834..a2660bafdf6a 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java
@@ -47,7 +47,6 @@
 import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
 import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;
 import org.junit.jupiter.params.aggregator.ArgumentsAggregator;
-import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor;
 import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator;
 import org.junit.jupiter.params.converter.ArgumentConverter;
 import org.junit.jupiter.params.converter.ConvertWith;
@@ -56,6 +55,7 @@
 import org.junit.jupiter.params.support.FieldContext;
 import org.junit.jupiter.params.support.ParameterDeclaration;
 import org.junit.jupiter.params.support.ParameterDeclarations;
+import org.junit.jupiter.params.support.ParameterInfo;
 import org.junit.platform.commons.JUnitException;
 import org.junit.platform.commons.PreconditionViolationException;
 import org.junit.platform.commons.function.Try;
@@ -457,10 +457,11 @@ private static ParameterResolutionException parameterResolutionException(String
 
 	private interface Resolver {
 
-		Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments,
-				int invocationIndex);
+		Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex);
 
-		Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex);
+		Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments,
+				int invocationIndex);
 
 	}
 
@@ -475,8 +476,8 @@ private static class Converter implements Resolver {
 		}
 
 		@Override
-		public Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments,
-				int invocationIndex) {
+		public Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex) {
 			Object argument = arguments.getConsumedPayload(parameterIndex);
 			try {
 				return this.argumentConverter.convert(argument, parameterContext);
@@ -487,7 +488,8 @@ public Object resolve(ParameterContext parameterContext, int parameterIndex, Eva
 		}
 
 		@Override
-		public Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex) {
+		public Object resolve(FieldContext fieldContext, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex) {
 			Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex());
 			try {
 				return this.argumentConverter.convert(argument, fieldContext);
@@ -515,10 +517,9 @@ protected Object aggregateArguments(ArgumentsAccessor accessor, Class<?> targetT
 		}
 
 		@Override
-		public Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments,
-				int invocationIndex) {
-			ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(parameterContext, invocationIndex,
-				arguments.getConsumedPayloads());
+		public Object resolve(ParameterContext parameterContext, int parameterIndex, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex) {
+			ArgumentsAccessor accessor = ParameterInfo.get(extensionContext).getArguments();
 			try {
 				return this.argumentsAggregator.aggregateArguments(accessor, parameterContext);
 			}
@@ -529,9 +530,9 @@ public Object resolve(ParameterContext parameterContext, int parameterIndex, Eva
 		}
 
 		@Override
-		public Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex) {
-			ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(fieldContext, invocationIndex,
-				arguments.getConsumedPayloads());
+		public Object resolve(FieldContext fieldContext, ExtensionContext extensionContext,
+				EvaluatedArgumentSet arguments, int invocationIndex) {
+			ArgumentsAccessor accessor = ParameterInfo.get(extensionContext).getArguments();
 			try {
 				return this.argumentsAggregator.aggregateArguments(accessor, fieldContext);
 			}
@@ -650,7 +651,7 @@ public Optional<String> getParameterName() {
 		@Override
 		public Object resolve(Resolver resolver, ExtensionContext extensionContext, EvaluatedArgumentSet arguments,
 				int invocationIndex, Optional<ParameterContext> originalParameterContext) {
-			return resolver.resolve(this, arguments, invocationIndex);
+			return resolver.resolve(this, extensionContext, arguments, invocationIndex);
 		}
 	}
 
@@ -692,7 +693,8 @@ public Object resolve(Resolver resolver, ExtensionContext extensionContext, Eval
 			ParameterContext parameterContext = originalParameterContext //
 					.filter(it -> it.getParameter().equals(this.parameter)) //
 					.orElseGet(() -> toParameterContext(extensionContext, originalParameterContext));
-			return resolver.resolve(parameterContext, getParameterIndex(), arguments, invocationIndex);
+			return resolver.resolve(parameterContext, getParameterIndex(), extensionContext, arguments,
+				invocationIndex);
 		}
 
 		private ParameterContext toParameterContext(ExtensionContext extensionContext,
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java
index cdb5c9d0806e..811a8abd0518 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java
@@ -19,9 +19,7 @@
 import java.util.function.BiFunction;
 
 import org.apiguardian.api.API;
-import org.junit.jupiter.api.extension.ParameterContext;
 import org.junit.jupiter.params.converter.DefaultArgumentConverter;
-import org.junit.jupiter.params.support.FieldContext;
 import org.junit.platform.commons.util.ClassUtils;
 import org.junit.platform.commons.util.Preconditions;
 
@@ -42,20 +40,11 @@ public class DefaultArgumentsAccessor implements ArgumentsAccessor {
 	private final Object[] arguments;
 	private final BiFunction<Object, Class<?>, Object> converter;
 
-	public static DefaultArgumentsAccessor create(ParameterContext parameterContext, int invocationIndex,
-			Object... arguments) {
-
-		Preconditions.notNull(parameterContext, "ParameterContext must not be null");
-		BiFunction<Object, Class<?>, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE //
-				.convert(source, targetType, parameterContext);
-		return new DefaultArgumentsAccessor(converter, invocationIndex, arguments);
-	}
-
-	public static DefaultArgumentsAccessor create(FieldContext fieldContext, int invocationIndex, Object... arguments) {
+	public static DefaultArgumentsAccessor create(int invocationIndex, ClassLoader classLoader, Object[] arguments) {
+		Preconditions.notNull(classLoader, "ClassLoader must not be null");
 
-		Preconditions.notNull(fieldContext, "FieldContext must not be null");
 		BiFunction<Object, Class<?>, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE //
-				.convert(source, targetType, fieldContext);
+				.convert(source, targetType, classLoader);
 		return new DefaultArgumentsAccessor(converter, invocationIndex, arguments);
 	}
 
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java
index af84d5da36f2..8544019c1894 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java
@@ -11,9 +11,9 @@
 package org.junit.jupiter.params.converter;
 
 import static org.apiguardian.api.API.Status.INTERNAL;
+import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader;
 
 import java.io.File;
-import java.lang.reflect.Member;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.net.URI;
@@ -27,7 +27,6 @@
 import org.junit.jupiter.params.support.FieldContext;
 import org.junit.platform.commons.support.conversion.ConversionException;
 import org.junit.platform.commons.support.conversion.ConversionSupport;
-import org.junit.platform.commons.util.ClassLoaderUtils;
 import org.junit.platform.commons.util.ReflectionUtils;
 
 /**
@@ -60,24 +59,18 @@ private DefaultArgumentConverter() {
 	@Override
 	public final Object convert(Object source, ParameterContext context) {
 		Class<?> targetType = context.getParameter().getType();
-		return convert(source, targetType, context);
+		ClassLoader classLoader = getClassLoader(context.getDeclaringExecutable().getDeclaringClass());
+		return convert(source, targetType, classLoader);
 	}
 
 	@Override
 	public final Object convert(Object source, FieldContext context) throws ArgumentConversionException {
 		Class<?> targetType = context.getField().getType();
-		return convert(source, targetType, context);
+		ClassLoader classLoader = getClassLoader(context.getField().getDeclaringClass());
+		return convert(source, targetType, classLoader);
 	}
 
-	public final Object convert(Object source, Class<?> targetType, ParameterContext context) {
-		return convert(source, targetType, context.getDeclaringExecutable());
-	}
-
-	public final Object convert(Object source, Class<?> targetType, FieldContext context) {
-		return convert(source, targetType, context.getField());
-	}
-
-	private Object convert(Object source, Class<?> targetType, Member member) {
+	public final Object convert(Object source, Class<?> targetType, ClassLoader classLoader) {
 		if (source == null) {
 			if (targetType.isPrimitive()) {
 				throw new ArgumentConversionException(
@@ -91,8 +84,6 @@ private Object convert(Object source, Class<?> targetType, Member member) {
 		}
 
 		if (source instanceof String) {
-			Class<?> declaringClass = member.getDeclaringClass();
-			ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass);
 			try {
 				return convert((String) source, targetType, classLoader);
 			}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java
new file mode 100644
index 000000000000..03fd6b2a811d
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterInfo.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.jupiter.params.support;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import org.apiguardian.api.API;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.extension.Extension;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
+
+/**
+ * {@code ParameterInfo} is used to provide information about the current
+ * invocation of a parameterized class or test.
+ *
+ * <p>Registered {@link Extension} implementations may retrieve the current
+ * {@code ParameterInfo} instance by calling
+ * {@link ExtensionContext#getStore(Namespace)} with {@link #NAMESPACE} and
+ * {@link ExtensionContext.Store#get(Object, Class) Store.get(...)} with
+ * {@link #KEY}. Alternatively, the {@link #get(ExtensionContext)} method may
+ * be used to retrieve the {@code ParameterInfo} instance for the supplied
+ * {@code ExtensionContext}. Extensions must not modify any entries in the
+ * {@link ExtensionContext.Store Store} for {@link #NAMESPACE}.
+ *
+ * <p>When a {@link ParameterizedTest @ParameterizedTest} method is declared
+ * inside a {@link ParameterizedClass @ParameterizedClass} or a
+ * {@link Nested @Nested} {@link ParameterizedClass @ParameterizedClass} is
+ * declared inside an enclosing {@link ParameterizedClass @ParameterizedClass},
+ * there will be multiple {@code ParameterInfo} instances available on different
+ * levels of the {@link ExtensionContext} hierarchy. In such cases, please use
+ * {@link ExtensionContext#getParent()} to navigate to the right level before
+ * retrieving the {@code ParameterInfo} instance from the
+ * {@link ExtensionContext.Store Store}.
+ *
+ *
+ * @since 5.13
+ * @see ParameterizedClass
+ * @see ParameterizedTest
+ */
+@API(status = EXPERIMENTAL, since = "5.13")
+public interface ParameterInfo {
+
+	/**
+	 * The {@link Namespace} for accessing the
+	 * {@link ExtensionContext.Store Store} for {@code ParameterInfo}.
+	 */
+	Namespace NAMESPACE = Namespace.create(ParameterInfo.class);
+
+	/**
+	 * The key for retrieving the {@code ParameterInfo} instance from the
+	 * {@link ExtensionContext.Store Store}.
+	 */
+	Object KEY = ParameterInfo.class;
+
+	/**
+	 * {@return the closest {@code ParameterInfo} instance for the supplied
+	 * {@code ExtensionContext}; potentially {@code null}}
+	 */
+	static ParameterInfo get(ExtensionContext context) {
+		return context.getStore(NAMESPACE).get(KEY, ParameterInfo.class);
+	}
+
+	/**
+	 * {@return the declarations of all <em>indexed</em> parameters}
+	 */
+	ParameterDeclarations getDeclarations();
+
+	/**
+	 * {@return the accessor to the arguments of the current invocation}
+	 */
+	ArgumentsAccessor getArguments();
+
+}


Test patch:
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java
index 5214e774ec39..7d48e198dfb2 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java
@@ -336,7 +336,8 @@ private static ParameterizedInvocationNameFormatter formatter(String pattern, St
 	}
 
 	private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName, Method method) {
-		var context = new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class));
+		var context = new ParameterizedTestContext(method.getDeclaringClass(), method,
+			method.getAnnotation(ParameterizedTest.class));
 		return new ParameterizedInvocationNameFormatter(pattern, displayName, context, 512);
 	}
 
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java
index 36f29fb2e55b..0baf170b2e56 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java
@@ -44,7 +44,7 @@ void invalidSignatures(String methodName) {
 
 	private ParameterizedTestContext createMethodContext(Class<?> testClass, String methodName) {
 		var method = ReflectionUtils.findMethods(testClass, m -> m.getName().equals(methodName)).getFirst();
-		return new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class));
+		return new ParameterizedTestContext(testClass, method, method.getAnnotation(ParameterizedTest.class));
 	}
 
 	@SuppressWarnings("JUnitMalformedDeclaration")
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 65aeef93efd9..472429698597 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
@@ -210,8 +210,8 @@ private ExtensionContext getExtensionContextReturningSingleMethod(Object testCas
 	private ExtensionContext getExtensionContextReturningSingleMethod(Object testCase,
 			Function<String, Optional<String>> configurationSupplier) {
 
-		var method = ReflectionUtils.findMethods(testCase.getClass(),
-			it -> "method".equals(it.getName())).stream().findFirst();
+		Class<?> testClass = testCase.getClass();
+		var method = ReflectionUtils.findMethods(testClass, it -> "method".equals(it.getName())).stream().findFirst();
 
 		return new ExtensionContext() {
 
@@ -254,7 +254,7 @@ public Optional<AnnotatedElement> getElement() {
 
 			@Override
 			public Optional<Class<?>> getTestClass() {
-				return Optional.empty();
+				return Optional.of(testClass);
 			}
 
 			@Override
@@ -308,7 +308,8 @@ public void publishDirectory(String name, ThrowingConsumer<Path> action) {
 			public Store getStore(Namespace namespace) {
 				var store = new NamespaceAwareStore(this.store, namespace);
 				method //
-						.map(it -> new ParameterizedTestContext(it, it.getAnnotation(ParameterizedTest.class))) //
+						.map(it -> new ParameterizedTestContext(testClass, it,
+							it.getAnnotation(ParameterizedTest.class))) //
 						.ifPresent(ctx -> store.put(DECLARATION_CONTEXT_KEY, ctx));
 				return store;
 			}
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java
index 09359a7b3769..792bae865f48 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java
@@ -16,16 +16,11 @@
 import static org.junit.jupiter.api.Assertions.assertIterableEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
-import java.lang.reflect.Method;
 import java.util.Arrays;
 
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ParameterContext;
 import org.junit.platform.commons.PreconditionViolationException;
-import org.junit.platform.commons.support.ReflectionSupport;
 
 /**
  * Unit tests for {@link DefaultArgumentsAccessor}.
@@ -169,14 +164,8 @@ void size() {
 	}
 
 	private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationIndex, Object... arguments) {
-		return DefaultArgumentsAccessor.create(parameterContext(), invocationIndex, arguments);
-	}
-
-	private static ParameterContext parameterContext() {
-		Method declaringExecutable = ReflectionSupport.findMethod(DefaultArgumentsAccessorTests.class, "foo").get();
-		ParameterContext parameterContext = mock();
-		when(parameterContext.getDeclaringExecutable()).thenReturn(declaringExecutable);
-		return parameterContext;
+		var classLoader = DefaultArgumentsAccessorTests.class.getClassLoader();
+		return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments);
 	}
 
 	@SuppressWarnings("unused")
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java
index b7e70fc1141e..5336690e1c1e 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java
@@ -16,21 +16,17 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.lang.reflect.Method;
 
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ParameterContext;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.junit.platform.commons.support.ReflectionSupport;
 import org.junit.platform.commons.support.conversion.ConversionException;
 import org.junit.platform.commons.test.TestClassLoader;
+import org.junit.platform.commons.util.ClassLoaderUtils;
 
 /**
  * Unit tests for {@link DefaultArgumentConverter}.
@@ -124,12 +120,12 @@ void delegatesStringToClassWithCustomTypeFromDifferentClassLoaderConversion() th
 			var customType = testClassLoader.loadClass(customTypeName);
 			assertThat(customType.getClassLoader()).isSameAs(testClassLoader);
 
-			var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").get();
+			var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").orElseThrow();
 			assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader);
 
 			doReturn(customType).when(underTest).convert(any(), any(), any(ClassLoader.class));
 
-			var clazz = (Class<?>) convert(customTypeName, Class.class, parameterContext(declaringExecutable));
+			var clazz = (Class<?>) convert(customTypeName, Class.class, testClassLoader);
 			assertThat(clazz).isNotEqualTo(Enigma.class);
 			assertThat(clazz).isEqualTo(customType);
 			assertThat(clazz.getClassLoader()).isSameAs(testClassLoader);
@@ -151,22 +147,11 @@ private void assertConverts(Object input, Class<?> targetClass, Object expectedO
 	}
 
 	private Object convert(Object input, Class<?> targetClass) {
-		return convert(input, targetClass, parameterContext());
-	}
-
-	private Object convert(Object input, Class<?> targetClass, ParameterContext parameterContext) {
-		return underTest.convert(input, targetClass, parameterContext);
-	}
-
-	private static ParameterContext parameterContext() {
-		Method declaringExecutable = ReflectionSupport.findMethod(DefaultArgumentConverterTests.class, "foo").get();
-		return parameterContext(declaringExecutable);
+		return convert(input, targetClass, ClassLoaderUtils.getClassLoader(getClass()));
 	}
 
-	private static ParameterContext parameterContext(Method declaringExecutable) {
-		ParameterContext parameterContext = mock();
-		when(parameterContext.getDeclaringExecutable()).thenReturn(declaringExecutable);
-		return parameterContext;
+	private Object convert(Object input, Class<?> targetClass, ClassLoader classLoader) {
+		return underTest.convert(input, targetClass, classLoader);
 	}
 
 	@SuppressWarnings("unused")
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java
new file mode 100644
index 000000000000..2b62f8d691e3
--- /dev/null
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/ParameterInfoIntegrationTests.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2015-2025 the original author or authors.
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v2.0 which
+ * accompanies this distribution and is available at
+ *
+ * https://www.eclipse.org/legal/epl-v20.html
+ */
+
+package org.junit.jupiter.params.support;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.BeforeClassTemplateInvocationCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
+import org.junit.jupiter.params.Parameter;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * @since 5.13
+ */
+class ParameterInfoIntegrationTests extends AbstractJupiterTestEngineTests {
+
+	@Test
+	void storesParameterInfoInExtensionContextStoreOnDifferentLevels() {
+		var results = executeTestsForClass(TestCase.class);
+
+		results.allEvents().assertStatistics(stats -> stats.started(7).succeeded(7));
+	}
+
+	@ParameterizedClass
+	@ValueSource(ints = 1)
+	@ExtendWith(ParameterInfoConsumingExtension.class)
+	record TestCase(int i) {
+
+		@Nested
+		@ParameterizedClass
+		@ValueSource(ints = 2)
+		class Inner {
+
+			@Parameter
+			int j;
+
+			@ParameterizedTest
+			@ValueSource(ints = 3)
+			void test(int k) {
+				assertEquals(1, i);
+				assertEquals(2, j);
+				assertEquals(3, k);
+			}
+		}
+	}
+
+	private static class ParameterInfoConsumingExtension
+			implements BeforeClassTemplateInvocationCallback, BeforeEachCallback {
+
+		@Override
+		public void beforeClassTemplateInvocation(ExtensionContext parameterizedClassInvocationContext) {
+			if (TestCase.Inner.class.equals(parameterizedClassInvocationContext.getRequiredTestClass())) {
+				assertParameterInfo(parameterizedClassInvocationContext, "j", 2);
+
+				var nestedParameterizedClassContext = parameterizedClassInvocationContext.getParent().orElseThrow();
+				assertParameterInfo(nestedParameterizedClassContext, "i", 1);
+
+				parameterizedClassInvocationContext = nestedParameterizedClassContext.getParent().orElseThrow();
+			}
+
+			assertParameterInfo(parameterizedClassInvocationContext, "i", 1);
+
+			var outerParameterizedClassContext = parameterizedClassInvocationContext.getParent().orElseThrow();
+			assertNull(ParameterInfo.get(outerParameterizedClassContext));
+		}
+
+		@Override
+		public void beforeEach(ExtensionContext parameterizedTestInvocationContext) {
+			assertParameterInfo(parameterizedTestInvocationContext, "k", 3);
+
+			var parameterizedTestContext = parameterizedTestInvocationContext.getParent().orElseThrow();
+			assertParameterInfo(parameterizedTestContext, "j", 2);
+
+			var nestedParameterizedClassInvocationContext = parameterizedTestContext.getParent().orElseThrow();
+			assertParameterInfo(nestedParameterizedClassInvocationContext, "j", 2);
+
+			var nestedParameterizedClassContext = nestedParameterizedClassInvocationContext.getParent().orElseThrow();
+			assertParameterInfo(nestedParameterizedClassContext, "i", 1);
+
+			var outerParameterizedClassInvocationContext = nestedParameterizedClassContext.getParent().orElseThrow();
+			assertParameterInfo(outerParameterizedClassInvocationContext, "i", 1);
+
+			var outerParameterizedClassContext = outerParameterizedClassInvocationContext.getParent().orElseThrow();
+			assertNull(ParameterInfo.get(outerParameterizedClassContext));
+		}
+
+		private static void assertParameterInfo(ExtensionContext context, String parameterName, int argumentValue) {
+			var parameterInfo = ParameterInfo.get(context);
+			var declaration = parameterInfo.getDeclarations().get(0).orElseThrow();
+			assertEquals(parameterName, declaration.getParameterName().orElseThrow());
+			assertEquals(int.class, declaration.getParameterType());
+			assertEquals(argumentValue, parameterInfo.getArguments().getInteger(0));
+		}
+	}
+}
diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt
index a83881e4753a..eb1aa43ed5fb 100644
--- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt
+++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt
@@ -13,11 +13,6 @@ import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.assertThrows
-import org.junit.jupiter.api.extension.ParameterContext
-import org.junit.platform.commons.util.ReflectionUtils
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.`when`
-import java.lang.reflect.Method
 
 /**
  * Unit tests for using [ArgumentsAccessor] from Kotlin.
@@ -56,13 +51,9 @@ class ArgumentsAccessorKotlinTests {
     fun defaultArgumentsAccessor(
         invocationIndex: Int,
         vararg arguments: Any
-    ): DefaultArgumentsAccessor = DefaultArgumentsAccessor.create(parameterContext(), invocationIndex, *arguments)
-
-    fun parameterContext(): ParameterContext {
-        val declaringExecutable: Method = ReflectionUtils.findMethod(DefaultArgumentsAccessorTests::class.java, "foo").get()
-        val parameterContext: ParameterContext = mock()
-        `when`(parameterContext.declaringExecutable).thenReturn(declaringExecutable)
-        return parameterContext
+    ): DefaultArgumentsAccessor {
+        val classLoader = ArgumentsAccessorKotlinTests::class.java.classLoader
+        return DefaultArgumentsAccessor.create(invocationIndex, classLoader, arguments)
     }
 
     fun foo() {