Parameter Resolution
ParameterResolver defines the Extension API for dynamically resolving parameters at
runtime.
If a test class constructor, test method, or lifecycle method (see
Definitions) declares a parameter, the parameter must be resolved at
runtime by a ParameterResolver. A ParameterResolver can either be built-in (see
) or registered by the user.
Generally speaking, parameters may be resolved by name, type, annotation, or any
combination thereof.
If you wish to implement a custom ParameterResolver that resolves parameters based
solely on the type of the parameter, you may find it convenient to extend the
which serves as a generic adapter for such use cases.
For concrete examples, consult the source code for , , and .
Due to a bug in the byte code generated by javac on JDK versions prior to JDK 9, looking up annotations on parameters directly via the core java.lang.reflect.Parameter API will always fail for inner class constructors (e.g., a constructor in a @Nested test class). The ParameterContext API supplied to ParameterResolver implementations therefore includes the following convenience methods for correctly looking up annotations on parameters. Extension authors are strongly encouraged to use these methods instead of those provided in java.lang.reflect.Parameter in order to avoid this bug in the JDK. - boolean isAnnotated(Class<? extends Annotation> annotationType) - Optional<A> findAnnotation(Class<A> annotationType) - List<A> findRepeatableAnnotations(Class<A> annotationType) |
Accessing the test-scoped ExtensionContext You may override the getTestInstantiationExtensionContextScope(…) method to return TEST_METHOD to support injecting test specific data into constructor parameters of the test class instance. Doing so causes a test-specific ExtensionContext to be used while resolving constructor parameters, unless the test instance lifecycle is set to PER_CLASS. |
Parameter resolution for methods called from extensions Other extensions can also leverage registered ParameterResolvers for method and constructor invocations, using the ExecutableInvoker available via the getExecutableInvoker() method in the ExtensionContext. |
Parameter Conflicts
If multiple implementations of ParameterResolver that support the same type are
registered for a test, a ParameterResolutionException will be thrown, with a
message to indicate that competing resolvers have been discovered. See the following
example:
Conflicting parameter resolution due to multiple resolvers claiming support for integers
- Java
- Kotlin
public class ParameterResolverConflictDemo {
@Test
@ExtendWith({ FirstIntegerResolver.class, SecondIntegerResolver.class })
void testInt(int i) {
// Test will not run due to ParameterResolutionException
assertEquals(1, i);
}
static class FirstIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == int.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 1;
}
}
static class SecondIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == int.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 2;
}
}
}
class ParameterResolverConflictDemo {
@Test
@ExtendWith(FirstIntegerResolver::class, SecondIntegerResolver::class)
fun testInt(i: Int) {
// Test will not run due to ParameterResolutionException
assertEquals(1, i)
}
class FirstIntegerResolver : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean = parameterContext.parameter.type == Int::class.javaPrimitiveType
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = 1
}
class SecondIntegerResolver : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean = parameterContext.parameter.type == Int::class.javaPrimitiveType
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = 2
}
}
If the conflicting ParameterResolver implementations are applied to different test
methods as shown in the following example, no conflict occurs.
Fine-grained registration to avoid conflict
- Java
- Kotlin
public class ParameterResolverNoConflictDemo {
@Test
@ExtendWith(FirstIntegerResolver.class)
void firstResolution(int i) {
assertEquals(1, i);
}
@Test
@ExtendWith(SecondIntegerResolver.class)
void secondResolution(int i) {
assertEquals(2, i);
}
static class FirstIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == int.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 1;
}
}
static class SecondIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == int.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 2;
}
}
}
class ParameterResolverNoConflictDemo {
@Test
@ExtendWith(FirstIntegerResolver::class)
fun firstResolution(i: Int) {
assertEquals(1, i)
}
@Test
@ExtendWith(SecondIntegerResolver::class)
fun secondResolution(i: Int) {
assertEquals(2, i)
}
class FirstIntegerResolver : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean = parameterContext.parameter.type == Int::class.javaPrimitiveType
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = 1
}
class SecondIntegerResolver : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean = parameterContext.parameter.type == Int::class.javaPrimitiveType
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = 2
}
}
If the conflicting ParameterResolver implementations need to be applied to the same test
method, you can implement a custom type or custom annotation as illustrated by
and , respectively.
Custom type to resolve duplicate types
- Java
- Kotlin
public class ParameterResolverCustomTypeDemo {
@Test
@ExtendWith({ FirstIntegerResolver.class, SecondIntegerResolver.class })
void testInt(Integer i, WrappedInteger wrappedInteger) {
assertEquals(1, i);
assertEquals(2, wrappedInteger.value);
}
static class FirstIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(Integer.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 1;
}
}
static class SecondIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(WrappedInteger.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return new WrappedInteger(2);
}
}
static class WrappedInteger {
private final int value;
WrappedInteger(int value) {
this.value = value;
}
}
}
class ParameterResolverCustomTypeDemo {
@Test
@ExtendWith(FirstIntegerResolver::class, SecondIntegerResolver::class)
fun testInt(
i: Int,
wrappedInteger: WrappedInteger
) {
assertEquals(1, i)
assertEquals(2, wrappedInteger.value)
}
class FirstIntegerResolver : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean = parameterContext.parameter.type == Int::class.javaPrimitiveType
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = 1
}
class SecondIntegerResolver : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean = parameterContext.parameter.type == WrappedInteger::class.java
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = WrappedInteger(2)
}
data class WrappedInteger(
val value: Int
)
}
A custom annotation makes the duplicate type distinguishable from its counterpart:
Custom annotation to resolve duplicate types
- Java
- Kotlin
public class ParameterResolverCustomAnnotationDemo {
@Test
void testInt(@FirstInteger Integer first, @SecondInteger Integer second) {
assertEquals(1, first);
assertEquals(2, second);
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(FirstInteger.Extension.class)
public @interface FirstInteger {
class Extension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(Integer.class)
&& !parameterContext.isAnnotated(SecondInteger.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 1;
}
}
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SecondInteger.Extension.class)
public @interface SecondInteger {
class Extension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.isAnnotated(SecondInteger.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 2;
}
}
}
}
class ParameterResolverCustomAnnotationDemo {
@Test
fun testInt(
@FirstInteger first: Int,
@SecondInteger second: Int
) {
assertEquals(1, first)
assertEquals(2, second)
}
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(FirstIntegerExtension::class)
annotation class FirstInteger
class FirstIntegerExtension : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean =
parameterContext.parameter.type == Int::class.javaPrimitiveType &&
!parameterContext.isAnnotated(SecondInteger::class.java)
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = 1
}
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(SecondIntegerExtension::class)
annotation class SecondInteger
class SecondIntegerExtension : ParameterResolver {
override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Boolean = parameterContext.isAnnotated(SecondInteger::class.java)
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext
): Any = 2
}
}
JUnit includes some built-in parameter resolvers that can cause conflicts if a resolver
attempts to claim their supported types. For example, TestInfo provides metadata about
tests. See Dependency Injection for Constructors and Methods for details. Third-party frameworks such
as Spring may also define parameter resolvers. Apply one of the techniques in this section
to resolve any conflicts.
Parameterized tests are another potential source of conflict. Ensure that tests annotated
with @ParameterizedTest are not also annotated with @Test and see
Consuming Arguments for more details.
