Repository: junit-team/junit-framework (Java)

Issue:
Add constructor injection support for `ArgumentsProvider`, `ArgumentConverter`, and `ArgumentsAggregator`
It is possible to provide parameters to the constructor of a test class, and also to a method used by `@ParameterizedTest`/`@MethodSource`.

Request: Be able to provide parameters to the constructor of an `ArgumentsProvider` implementation.

Sample:

```java
@ExtendWith(MyExtension.class) // this extension provides values for type 'Pojo' parameters
public class WithArgumentsProvider {

    @Nested
    public class NestedTest1 {

        @ParameterizedTest
        @ArgumentsSource(MyArgumentsProvider.class)
        public void test1(String a) {
            System.out.println("NestedTest1 - Test1: " + a);
        }

    }

    @Nested
    public class NestedTest2 {

        @ParameterizedTest
        @ArgumentsSource(MyArgumentsProvider.class)
        public void test2(String a) {
            System.out.println("NestedTest2 - Test2: " + a);
        }

    }

    public static class MyArgumentsProvider implements ArgumentsProvider {

        public MyArgumentsProvider(Pojo pojo) {
            // ...
        }

        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
            return Stream.of("a", "b", "c").map(Arguments::of);
        }

    }

}
```

Dev hint: https://github.com/junit-team/junit5/blob/e8c92f8cdeafe80ae485da4b4d8bd77f7b33e345/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java#L87




Discussion: In the meantime, as a workaround, you can do this:

```java
@ExtendWith(WithArgumentsProvider.MyExtension.class) // this extension provides values for type 'Pojo' parameters
public class WithArgumentsProvider {

    @Nested
    public class NestedTest1 {

        @ParameterizedTest
        @ArgumentsSource(MyArgumentsProvider.class)
        public void test1(String a) {
            System.out.println("NestedTest1 - Test1: " + a);
        }

    }

    @Nested
    public class NestedTest2 {

        @ParameterizedTest
        @ArgumentsSource(MyArgumentsProvider.class)
        public void test2(String a) {
            System.out.println("NestedTest2 - Test2: " + a);
        }

    }

    public static class MyArgumentsProvider implements ArgumentsProvider {

        @SuppressWarnings("unchecked")
		@Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
            Method method = getClass().getDeclaredMethod("provideArguments", Pojo.class);
			return (Stream<? extends Arguments>) context.getExecutableInvoker().invoke(method, this);
        }

        Stream<? extends Arguments> provideArguments(Pojo pojo) {
            return Stream.of(Arguments.of(pojo.toString()));
        }
    }

}

```
I think the only reason that's not implemented is because `@ParameterizedTest` is implemented as an extension and predates `ExecutableInvoker`. Adding support should be as simple as changing
https://github.com/junit-team/junit5/blob/c24d9a630282a57bf210539b1591115bd988e19a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java#L103
to use `context.getExecutableInvoker().invoke(clazz.getDeclaredConstructor())`.
Related issue:

- #2191
> Adding support should be as simple as changing
> 
> https://github.com/junit-team/junit5/blob/c24d9a630282a57bf210539b1591115bd988e19a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java#L103

As usual, it turned out to be slightly more complicated than that to preserve backward compatibility.



Fix (patch):
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
index c37680f13b5d..e23dcb707c34 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
@@ -50,6 +50,10 @@ JUnit repository on GitHub.
 * In a `@ParameterizedTest` method, a `null` value can now be supplied for Java Date/Time
   types such as `LocalDate` if the new `nullable` attribute in
   `@JavaTimeConversionPattern` is set to `true`.
+* `ArgumentsProvider` (declared via `@ArgumentsSource`), `ArgumentConverter` (declared via
+  `@ConvertWith`), and `ArgumentsAggregator` (declared via `@AggregateWith`)
+  implementations can now use constructor injection from registered `ParameterResolver`
+  extensions.
 
 
 [[release-notes-5.12.0-M1-junit-vintage]]
diff --git a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java
index bc296054ec36..e3e7df3c58c2 100644
--- a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java
+++ b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java
@@ -47,7 +47,8 @@ public void formatTestNames(Blackhole blackhole) throws Exception {
 		var formatter = new ParameterizedTestNameFormatter(
 			ParameterizedTest.DISPLAY_NAME_PLACEHOLDER + " " + ParameterizedTest.DEFAULT_DISPLAY_NAME + " ({0})",
 			"displayName",
-			new ParameterizedTestMethodContext(TestCase.class.getDeclaredMethod("parameterizedTest", int.class)), 512);
+			new ParameterizedTestMethodContext(TestCase.class.getDeclaredMethod("parameterizedTest", int.class), null),
+			512);
 		for (int i = 0; i < argumentsList.size(); i++) {
 			Arguments arguments = argumentsList.get(i);
 			blackhole.consume(formatter.format(i, arguments, arguments.get()));
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 f625faac7f17..aefbba1f2adc 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
@@ -26,8 +26,6 @@
 import org.junit.jupiter.params.provider.ArgumentsProvider;
 import org.junit.jupiter.params.provider.ArgumentsSource;
 import org.junit.jupiter.params.support.AnnotationConsumerInitializer;
-import org.junit.platform.commons.JUnitException;
-import org.junit.platform.commons.support.ReflectionSupport;
 import org.junit.platform.commons.util.ExceptionUtils;
 import org.junit.platform.commons.util.Preconditions;
 
@@ -52,7 +50,7 @@ public boolean supportsTestTemplate(ExtensionContext context) {
 			return false;
 		}
 
-		ParameterizedTestMethodContext methodContext = new ParameterizedTestMethodContext(testMethod);
+		ParameterizedTestMethodContext methodContext = new ParameterizedTestMethodContext(testMethod, context);
 
 		Preconditions.condition(methodContext.hasPotentiallyValidSignature(),
 			() -> String.format(
@@ -84,7 +82,7 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
 		return findRepeatableAnnotations(templateMethod, ArgumentsSource.class)
 				.stream()
 				.map(ArgumentsSource::value)
-				.map(this::instantiateArgumentsProvider)
+				.map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsProvider.class, clazz, extensionContext))
 				.map(provider -> AnnotationConsumerInitializer.initialize(templateMethod, provider))
 				.flatMap(provider -> arguments(provider, extensionContext))
 				.map(arguments -> {
@@ -97,23 +95,6 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
 		// @formatter:on
 	}
 
-	@SuppressWarnings("ConstantConditions")
-	private ArgumentsProvider instantiateArgumentsProvider(Class<? extends ArgumentsProvider> clazz) {
-		try {
-			return ReflectionSupport.newInstance(clazz);
-		}
-		catch (Exception ex) {
-			if (ex instanceof NoSuchMethodException) {
-				String message = String.format("Failed to find a no-argument constructor for ArgumentsProvider [%s]. "
-						+ "Please ensure that a no-argument constructor exists and "
-						+ "that the class is either a top-level class or a static nested class",
-					clazz.getName());
-				throw new JUnitException(message, ex);
-			}
-			throw ex;
-		}
-	}
-
 	private ExtensionContext.Store getStore(ExtensionContext context) {
 		return context.getStore(Namespace.create(ParameterizedTestExtension.class, context.getRequiredTestMethod()));
 	}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java
index 1b88b3a14467..a7469d4f4003 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java
@@ -20,6 +20,7 @@
 import java.util.List;
 import java.util.Optional;
 
+import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ParameterContext;
 import org.junit.jupiter.api.extension.ParameterResolutionException;
 import org.junit.jupiter.params.aggregator.AggregateWith;
@@ -31,7 +32,6 @@
 import org.junit.jupiter.params.converter.DefaultArgumentConverter;
 import org.junit.jupiter.params.support.AnnotationConsumerInitializer;
 import org.junit.platform.commons.support.AnnotationSupport;
-import org.junit.platform.commons.support.ReflectionSupport;
 import org.junit.platform.commons.util.StringUtils;
 
 /**
@@ -43,16 +43,18 @@
 class ParameterizedTestMethodContext {
 
 	private final Parameter[] parameters;
+	private final ExtensionContext extensionContext;
 	private final Resolver[] resolvers;
 	private final List<ResolverType> resolverTypes;
 
-	ParameterizedTestMethodContext(Method testMethod) {
+	ParameterizedTestMethodContext(Method testMethod, ExtensionContext extensionContext) {
 		this.parameters = testMethod.getParameters();
 		this.resolvers = new Resolver[this.parameters.length];
 		this.resolverTypes = new ArrayList<>(this.parameters.length);
 		for (Parameter parameter : this.parameters) {
 			this.resolverTypes.add(isAggregator(parameter) ? AGGREGATOR : CONVERTER);
 		}
+		this.extensionContext = extensionContext;
 	}
 
 	/**
@@ -167,7 +169,7 @@ Object resolve(ParameterContext parameterContext, Object[] arguments, int invoca
 	private Resolver getResolver(ParameterContext parameterContext) {
 		int index = parameterContext.getIndex();
 		if (resolvers[index] == null) {
-			resolvers[index] = resolverTypes.get(index).createResolver(parameterContext);
+			resolvers[index] = resolverTypes.get(index).createResolver(parameterContext, extensionContext);
 		}
 		return resolvers[index];
 	}
@@ -176,11 +178,11 @@ enum ResolverType {
 
 		CONVERTER {
 			@Override
-			Resolver createResolver(ParameterContext parameterContext) {
+			Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext) {
 				try { // @formatter:off
 					return AnnotationSupport.findAnnotation(parameterContext.getParameter(), ConvertWith.class)
 							.map(ConvertWith::value)
-							.map(clazz -> (ArgumentConverter) ReflectionSupport.newInstance(clazz))
+							.map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext))
 							.map(converter -> AnnotationConsumerInitializer.initialize(parameterContext.getParameter(), converter))
 							.map(Converter::new)
 							.orElse(Converter.DEFAULT);
@@ -193,11 +195,11 @@ Resolver createResolver(ParameterContext parameterContext) {
 
 		AGGREGATOR {
 			@Override
-			Resolver createResolver(ParameterContext parameterContext) {
+			Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext) {
 				try { // @formatter:off
 					return AnnotationSupport.findAnnotation(parameterContext.getParameter(), AggregateWith.class)
 							.map(AggregateWith::value)
-							.map(clazz -> (ArgumentsAggregator) ReflectionSupport.newInstance(clazz))
+							.map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsAggregator.class, clazz, extensionContext))
 							.map(Aggregator::new)
 							.orElse(Aggregator.DEFAULT);
 				} // @formatter:on
@@ -207,7 +209,7 @@ Resolver createResolver(ParameterContext parameterContext) {
 			}
 		};
 
-		abstract Resolver createResolver(ParameterContext parameterContext);
+		abstract Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext);
 
 	}
 
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestSpiInstantiator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestSpiInstantiator.java
new file mode 100644
index 000000000000..24c7163ac27a
--- /dev/null
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestSpiInstantiator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2015-2024 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 static org.junit.platform.commons.util.CollectionUtils.getFirstElement;
+
+import java.lang.reflect.Constructor;
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.PreconditionViolationException;
+import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.commons.util.ReflectionUtils;
+
+/**
+ * @since 5.12
+ */
+class ParameterizedTestSpiInstantiator {
+
+	static <T> T instantiate(Class<T> spiInterface, Class<? extends T> implementationClass,
+			ExtensionContext extensionContext) {
+		return extensionContext.getExecutableInvoker() //
+				.invoke(findConstructor(spiInterface, implementationClass));
+	}
+
+	/**
+	 * Find the "best" constructor for the supplied class.
+	 *
+	 * <p>For backward compatibility, it first checks for a default constructor
+	 * which takes precedence over any other constructor. If no default
+	 * constructor is found, it checks for a single constructor and returns it.
+	 */
+	private static <T, V extends T> Constructor<? extends V> findConstructor(Class<T> spiInterface,
+			Class<V> implementationClass) {
+
+		Preconditions.condition(!ReflectionUtils.isInnerClass(implementationClass),
+			() -> String.format("The %s [%s] must be either a top-level class or a static nested class",
+				spiInterface.getSimpleName(), implementationClass.getName()));
+
+		return findDefaultConstructor(implementationClass) //
+				.orElseGet(() -> findSingleConstructor(spiInterface, implementationClass));
+	}
+
+	@SuppressWarnings("unchecked")
+	private static <T> Optional<Constructor<T>> findDefaultConstructor(Class<T> clazz) {
+		return getFirstElement(ReflectionUtils.findConstructors(clazz, it -> it.getParameterCount() == 0)) //
+				.map(it -> (Constructor<T>) it);
+	}
+
+	private static <T, V extends T> Constructor<V> findSingleConstructor(Class<T> spiInterface,
+			Class<V> implementationClass) {
+
+		try {
+			return ReflectionUtils.getDeclaredConstructor(implementationClass);
+		}
+		catch (PreconditionViolationException ex) {
+			String message = String.format(
+				"Failed to find constructor for %s [%s]. "
+						+ "Please ensure that a no-argument or a single constructor exists.",
+				spiInterface.getSimpleName(), implementationClass.getName());
+			throw new JUnitException(message);
+		}
+	}
+}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java
index e4ed9803f09f..c82d12cda961 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java
@@ -14,6 +14,7 @@
 
 import org.apiguardian.api.API;
 import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
 
 /**
  * {@code ArgumentsAggregator} is an abstraction for the aggregation of arguments
@@ -33,10 +34,11 @@
  * in a CSV file into a domain object such as a {@code Person}, {@code Address},
  * {@code Order}, etc.
  *
- * <p>Implementations must provide a no-args constructor and should not make any
- * assumptions regarding when they are instantiated or how often they are called.
- * Since instances may potentially be cached and called from different threads,
- * they should be thread-safe and designed to be used as singletons.
+ * <p>Implementations must provide a no-args constructor or a single unambiguous
+ * constructor to use {@linkplain ParameterResolver parameter resolution}. They
+ * should not make any assumptions regarding when they are instantiated or how
+ * often they are called. Since instances may potentially be cached and called
+ * from different threads, they should be thread-safe.
  *
  * @since 5.2
  * @see AggregateWith
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java
index 46f7e9a84b79..b18d96887069 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java
@@ -14,6 +14,7 @@
 
 import org.apiguardian.api.API;
 import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
 
 /**
  * {@code ArgumentConverter} is an abstraction that allows an input object to
@@ -24,10 +25,11 @@
  * method with the help of a
  * {@link org.junit.jupiter.params.converter.ConvertWith @ConvertWith} annotation.
  *
- * <p>Implementations must provide a no-args constructor and should not make any
- * assumptions regarding when they are instantiated or how often they are called.
- * Since instances may potentially be cached and called from different threads,
- * they should be thread-safe and designed to be used as singletons.
+ * <p>Implementations must provide a no-args constructor or a single unambiguous
+ * constructor to use {@linkplain ParameterResolver parameter resolution}. They
+ * should not make any assumptions regarding when they are instantiated or how
+ * often they are called. Since instances may potentially be cached and called
+ * from different threads, they should be thread-safe.
  *
  * <p>Extend {@link SimpleArgumentConverter} if your implementation only needs
  * to know the target type and does not need access to the {@link ParameterContext}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java
index 689517d607b8..c18d100ad3c5 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java
@@ -16,6 +16,7 @@
 
 import org.apiguardian.api.API;
 import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
 
 /**
  * An {@code ArgumentsProvider} is responsible for {@linkplain #provideArguments
@@ -25,7 +26,8 @@
  * <p>An {@code ArgumentsProvider} can be registered via the
  * {@link ArgumentsSource @ArgumentsSource} annotation.
  *
- * <p>Implementations must provide a no-args constructor.
+ * <p>Implementations must provide a no-args constructor or a single unambiguous
+ * constructor to use {@linkplain ParameterResolver parameter resolution}.
  *
  * @since 5.0
  * @see org.junit.jupiter.params.ParameterizedTest