From 1c40f91c16ef6aaba95a4535b84f957177066dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=9B=88?= Date: Sat, 25 Oct 2025 01:39:07 +0900 Subject: [PATCH 1/6] fix: validate argument count for @ParameterizedClass field injection (#5079) --- .../params/ParameterizedClassContext.java | 66 +++++++++++++++++++ .../ParameterizedClassIntegrationTests.java | 30 +++++++++ 2 files changed, 96 insertions(+) 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 f0402064a5a8..499d40661635 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 @@ -27,7 +27,9 @@ import java.util.List; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.params.provider.Arguments; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.support.HierarchyTraversalMode; @@ -130,6 +132,11 @@ public ResolverFacade getResolverFacade() { @Override public ParameterizedClassInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex) { + + if (this.injectionType == InjectionType.FIELDS) { + assertEnoughArgumentsForFieldInjection(arguments); + } + return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex); } @@ -172,6 +179,65 @@ private static A getAnnotation(Method method, Class an .orElseThrow(() -> new JUnitException("Method not annotated with @" + annotationType.getSimpleName())); } + private void assertEnoughArgumentsForFieldInjection(Arguments arguments) { + @SuppressWarnings("NullAway") + final Object[] providedArguments = (arguments == null ? new Object[0] : arguments.get()); + final int providedArgumentCount = providedArguments.length; + + final List parameterFields = findParameterAnnotatedFields(this.testClass); + if (parameterFields.isEmpty()) { + return; + } + + final int requiredArgumentCount = requiredArgumentCountForParameterFields(parameterFields); + if (providedArgumentCount >= requiredArgumentCount) { + return; + } + + final @Nullable Field firstMissingField = firstMissingParameterFieldByIndex(parameterFields, + providedArgumentCount); + + final String missingTargetDescription = (firstMissingField != null) + ? "field '%s' (index %d, type %s)".formatted(firstMissingField.getName(), + firstMissingField.getAnnotation(Parameter.class).value(), firstMissingField.getType().getName()) + : "parameter at index %d".formatted(providedArgumentCount); + + throw new ParameterResolutionException( + "Not enough arguments for @ParameterizedClass field injection in %s: %s cannot be injected because only %d argument(s) were provided; at least %d are required.".formatted( + this.testClass.getName(), missingTargetDescription, providedArgumentCount, requiredArgumentCount)); + } + + private static int requiredArgumentCountForParameterFields(List parameterFields) { + int maxIndex = -1; + for (Field field : parameterFields) { + Parameter param = field.getAnnotation(Parameter.class); + if (param != null) { + int index = param.value(); + if (index >= 0 && index > maxIndex) { + maxIndex = index; + } + } + } + return maxIndex + 1; + } + + private static @Nullable Field firstMissingParameterFieldByIndex(List parameterFields, + int providedArgumentCount) { + Field candidate = null; + int minIndex = Integer.MAX_VALUE; + for (Field field : parameterFields) { + Parameter param = field.getAnnotation(Parameter.class); + if (param != null) { + int index = param.value(); + if (index >= 0 && index >= providedArgumentCount && index < minIndex) { + candidate = field; + minIndex = index; + } + } + } + return candidate; + } + enum InjectionType { CONSTRUCTOR, FIELDS } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java index fea99691a7a9..b6c1caaa2dc7 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -552,6 +552,20 @@ void declaredIndexMustBeUnique() { "Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted( classTemplateClass.getName(), classTemplateClass.getName())))); } + + @Test + void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() { + var results = executeTestsForClass(NotEnoughArgumentsForFieldsTestCase.class); + + results.containerEvents().assertThatEvents() + .haveExactly(1, event(finishedWithFailure( + instanceOf(org.junit.jupiter.api.extension.ParameterResolutionException.class), + message(it -> it.contains("field 's") + && it.contains("index 1") + && it.contains("only 1 argument") + && it.contains("at least 2")) + ))); + } } @Nested @@ -1693,6 +1707,22 @@ void test() { } } + @ParameterizedClass + @ValueSource(ints = 1) + static class NotEnoughArgumentsForFieldsTestCase { + + @Parameter(0) + int i; + + @Parameter(1) + String s; + + @org.junit.jupiter.api.Test + void test() { + fail("should not be called"); + } + } + @ParameterizedClass @CsvSource({ "unused1, foo, unused2, bar", "unused4, baz, unused5, qux" }) static class InvalidUnusedParameterIndexesTestCase { From 3151b710817f04920ec5167205f4cf92ead7c814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=A7=80=ED=9B=88?= Date: Sat, 25 Oct 2025 18:39:44 +0900 Subject: [PATCH 2/6] fix: validate argument count for ParameterizedClass field injection --- .../params/ParameterizedClassContext.java | 65 ------------------- .../ParameterizedClassInvocationContext.java | 2 + .../junit/jupiter/params/ResolverFacade.java | 39 +++++++++++ .../ParameterizedClassIntegrationTests.java | 2 +- 4 files changed, 42 insertions(+), 66 deletions(-) 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 499d40661635..67f40bbc649b 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 @@ -27,9 +27,7 @@ import java.util.List; import java.util.function.Predicate; -import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.params.provider.Arguments; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.support.HierarchyTraversalMode; @@ -133,10 +131,6 @@ public ResolverFacade getResolverFacade() { public ParameterizedClassInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex) { - if (this.injectionType == InjectionType.FIELDS) { - assertEnoughArgumentsForFieldInjection(arguments); - } - return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex); } @@ -179,65 +173,6 @@ private static A getAnnotation(Method method, Class an .orElseThrow(() -> new JUnitException("Method not annotated with @" + annotationType.getSimpleName())); } - private void assertEnoughArgumentsForFieldInjection(Arguments arguments) { - @SuppressWarnings("NullAway") - final Object[] providedArguments = (arguments == null ? new Object[0] : arguments.get()); - final int providedArgumentCount = providedArguments.length; - - final List parameterFields = findParameterAnnotatedFields(this.testClass); - if (parameterFields.isEmpty()) { - return; - } - - final int requiredArgumentCount = requiredArgumentCountForParameterFields(parameterFields); - if (providedArgumentCount >= requiredArgumentCount) { - return; - } - - final @Nullable Field firstMissingField = firstMissingParameterFieldByIndex(parameterFields, - providedArgumentCount); - - final String missingTargetDescription = (firstMissingField != null) - ? "field '%s' (index %d, type %s)".formatted(firstMissingField.getName(), - firstMissingField.getAnnotation(Parameter.class).value(), firstMissingField.getType().getName()) - : "parameter at index %d".formatted(providedArgumentCount); - - throw new ParameterResolutionException( - "Not enough arguments for @ParameterizedClass field injection in %s: %s cannot be injected because only %d argument(s) were provided; at least %d are required.".formatted( - this.testClass.getName(), missingTargetDescription, providedArgumentCount, requiredArgumentCount)); - } - - private static int requiredArgumentCountForParameterFields(List parameterFields) { - int maxIndex = -1; - for (Field field : parameterFields) { - Parameter param = field.getAnnotation(Parameter.class); - if (param != null) { - int index = param.value(); - if (index >= 0 && index > maxIndex) { - maxIndex = index; - } - } - } - return maxIndex + 1; - } - - private static @Nullable Field firstMissingParameterFieldByIndex(List parameterFields, - int providedArgumentCount) { - Field candidate = null; - int minIndex = Integer.MAX_VALUE; - for (Field field : parameterFields) { - Parameter param = field.getAnnotation(Parameter.class); - if (param != null) { - int index = param.value(); - if (index >= 0 && index >= providedArgumentCount && index < minIndex) { - candidate = field; - minIndex = index; - } - } - } - return candidate; - } - enum InjectionType { CONSTRUCTOR, FIELDS } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java index b4d8a5e63122..e52be9f902c7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java @@ -65,6 +65,8 @@ private ClassTemplateConstructorParameterResolver createExtensionForConstructorI } private Extension createExtensionForFieldInjection() { + this.declarationContext.getResolverFacade().assertEnoughFieldArguments(this.arguments); + ResolverFacade resolverFacade = this.declarationContext.getResolverFacade(); TestInstance.Lifecycle lifecycle = this.declarationContext.getTestInstanceLifecycle(); return switch (lifecycle) { 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 29d1cd9dae5c..3054dead4657 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 @@ -464,6 +464,43 @@ private static ParameterResolutionException parameterResolutionException(String return new ParameterResolutionException(fullMessage, cause); } + private static void assertFieldAvailableOrThrow(FieldContext fieldContext, EvaluatedArgumentSet arguments) { + int parameterIndex = fieldContext.getParameterIndex(); + int provided = arguments.getTotalLength(); + int required = parameterIndex + 1; + + if (provided < required) { + Field field = fieldContext.getField(); + throw new org.junit.jupiter.api.extension.ParameterResolutionException( + ("Not enough arguments for @ParameterizedClass field injection: " + + "field '%s' (index %d, type %s) cannot be injected because " + + "only %d argument(s) were provided; at least %d are required.").formatted(field.getName(), + parameterIndex, field.getType().getName(), provided, required)); + } + } + + void assertEnoughFieldArguments(EvaluatedArgumentSet arguments) { + int provided = arguments.getTotalLength(); + int required = determineConsumedArgumentLength(Integer.MAX_VALUE); + + if (provided < required) { + int missingIndex = provided; + var maybeDecl = getIndexedParameterDeclarations().get(missingIndex); + if (maybeDecl.isPresent() && maybeDecl.get() instanceof FieldParameterDeclaration fpd) { + Field field = fpd.getField(); + throw new org.junit.jupiter.api.extension.ParameterResolutionException( + ("Not enough arguments for @ParameterizedClass field injection: " + + "field '%s' (index %d, type %s) cannot be injected because " + + "only %d argument(s) were provided; at least %d are required.").formatted(field.getName(), + missingIndex, field.getType().getName(), provided, required)); + } + throw new org.junit.jupiter.api.extension.ParameterResolutionException( + "Not enough arguments for @ParameterizedClass field injection: " + + "index %d cannot be injected because only %d argument(s) were provided; at least %d are required.".formatted( + missingIndex, provided, required)); + } + } + private interface Resolver { @Nullable @@ -495,6 +532,8 @@ private record Converter(ArgumentConverter argumentConverter) implements Resolve @Override public @Nullable Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, int invocationIndex) { + ResolverFacade.assertFieldAvailableOrThrow(fieldContext, arguments); + Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex()); try { return this.argumentConverter.convert(argument, fieldContext); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java index b6c1caaa2dc7..20c7bca7b60c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -560,7 +560,7 @@ void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() { results.containerEvents().assertThatEvents() .haveExactly(1, event(finishedWithFailure( instanceOf(org.junit.jupiter.api.extension.ParameterResolutionException.class), - message(it -> it.contains("field 's") + message(it -> it.contains("field 's'") && it.contains("index 1") && it.contains("only 1 argument") && it.contains("at least 2")) From 8f6587d3e56b97aa942bd1923570bf80d342ffd4 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 27 Oct 2025 11:39:48 +0100 Subject: [PATCH 3/6] Move required parameter validation into `ArgumentCountValidator` --- .../params/ArgumentCountValidator.java | 29 ++++++++-- .../ParameterizedClassInvocationContext.java | 2 - .../junit/jupiter/params/ResolverFacade.java | 57 ++++++------------- .../ParameterizedClassIntegrationTests.java | 13 ++--- 4 files changed, 45 insertions(+), 56 deletions(-) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java index eabcb067044f..cbb1330f53a9 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; @@ -36,6 +37,7 @@ class ArgumentCountValidator { } void validate(ExtensionContext extensionContext) { + validateRequiredArgumentsArePresent(); ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext); switch (argumentCountValidationMode) { case DEFAULT, NONE -> { @@ -45,17 +47,34 @@ void validate(ExtensionContext extensionContext) { this.arguments); int totalCount = this.arguments.getTotalLength(); Preconditions.condition(consumedCount == totalCount, - () -> "Configuration error: @%s consumes %s %s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted( - this.declarationContext.getAnnotationName(), consumedCount, - pluralize(consumedCount, "parameter", "parameters"), pluralize(totalCount, "was", "were"), - totalCount, pluralize(totalCount, "argument", "arguments"), - Arrays.toString(this.arguments.getAllPayloads()))); + () -> wrongNumberOfArgumentsMessages("consumes", consumedCount, null, null)); } default -> throw new ExtensionConfigurationException( "Unsupported argument count validation mode: " + argumentCountValidationMode); } } + private void validateRequiredArgumentsArePresent() { + var requiredParameterCount = this.declarationContext.getResolverFacade().getRequiredParameterCount(); + if (requiredParameterCount != null) { + var totalCount = this.arguments.getTotalLength(); + Preconditions.condition(requiredParameterCount.value() <= totalCount, + () -> wrongNumberOfArgumentsMessages("has", requiredParameterCount.value(), "required", + requiredParameterCount.reason())); + } + } + + private String wrongNumberOfArgumentsMessages(String verb, int actualCount, @Nullable String parameterAdjective, + @Nullable String reason) { + int totalCount = this.arguments.getTotalLength(); + return "Configuration error: @%s %s %s %s%s%s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted( + this.declarationContext.getAnnotationName(), verb, actualCount, + parameterAdjective == null ? "" : parameterAdjective + " ", + pluralize(actualCount, "parameter", "parameters"), reason == null ? "" : " (due to %s)".formatted(reason), + pluralize(totalCount, "was", "were"), totalCount, pluralize(totalCount, "argument", "arguments"), + Arrays.toString(this.arguments.getAllPayloads())); + } + private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) { ArgumentCountValidationMode mode = declarationContext.getArgumentCountValidationMode(); if (mode != ArgumentCountValidationMode.DEFAULT) { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java index e52be9f902c7..b4d8a5e63122 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java @@ -65,8 +65,6 @@ private ClassTemplateConstructorParameterResolver createExtensionForConstructorI } private Extension createExtensionForFieldInjection() { - this.declarationContext.getResolverFacade().assertEnoughFieldArguments(this.arguments); - ResolverFacade resolverFacade = this.declarationContext.getResolverFacade(); TestInstance.Lifecycle lifecycle = this.declarationContext.getTestInstanceLifecycle(); return switch (lifecycle) { 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 3054dead4657..1eb0c2d721f3 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 @@ -98,7 +98,9 @@ static ResolverFacade create(Class clazz, List fields) { Stream.concat(uniqueIndexedParameters.values().stream(), aggregatorParameters.stream()) // .forEach(declaration -> makeAccessible(declaration.getField())); - return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0); + var requiredParameterCount = new RequiredParameterCount(uniqueIndexedParameters.size(), "field injection"); + + return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0, requiredParameterCount); } static ResolverFacade create(Constructor constructor, ParameterizedClass annotation) { @@ -155,27 +157,35 @@ else if (aggregatorParameters.isEmpty()) { } } return new ResolverFacade(executable, indexedParameters, new LinkedHashSet<>(aggregatorParameters.values()), - indexOffset); + indexOffset, null); } private final int parameterIndexOffset; private final Map resolvers; private final DefaultParameterDeclarations indexedParameterDeclarations; private final Set aggregatorParameters; + private final @Nullable RequiredParameterCount requiredParameterCount; private ResolverFacade(AnnotatedElement sourceElement, NavigableMap indexedParameters, - Set aggregatorParameters, int parameterIndexOffset) { + Set aggregatorParameters, int parameterIndexOffset, + @Nullable RequiredParameterCount requiredParameterCount) { this.aggregatorParameters = aggregatorParameters; this.parameterIndexOffset = parameterIndexOffset; this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size()); this.indexedParameterDeclarations = new DefaultParameterDeclarations(sourceElement, indexedParameters); + this.requiredParameterCount = requiredParameterCount; } ParameterDeclarations getIndexedParameterDeclarations() { return this.indexedParameterDeclarations; } + @Nullable + RequiredParameterCount getRequiredParameterCount() { + return this.requiredParameterCount; + } + boolean isSupportedParameter(ParameterContext parameterContext, EvaluatedArgumentSet arguments) { int index = toLogicalIndex(parameterContext); if (this.indexedParameterDeclarations.get(index).isPresent()) { @@ -464,43 +474,6 @@ private static ParameterResolutionException parameterResolutionException(String return new ParameterResolutionException(fullMessage, cause); } - private static void assertFieldAvailableOrThrow(FieldContext fieldContext, EvaluatedArgumentSet arguments) { - int parameterIndex = fieldContext.getParameterIndex(); - int provided = arguments.getTotalLength(); - int required = parameterIndex + 1; - - if (provided < required) { - Field field = fieldContext.getField(); - throw new org.junit.jupiter.api.extension.ParameterResolutionException( - ("Not enough arguments for @ParameterizedClass field injection: " - + "field '%s' (index %d, type %s) cannot be injected because " - + "only %d argument(s) were provided; at least %d are required.").formatted(field.getName(), - parameterIndex, field.getType().getName(), provided, required)); - } - } - - void assertEnoughFieldArguments(EvaluatedArgumentSet arguments) { - int provided = arguments.getTotalLength(); - int required = determineConsumedArgumentLength(Integer.MAX_VALUE); - - if (provided < required) { - int missingIndex = provided; - var maybeDecl = getIndexedParameterDeclarations().get(missingIndex); - if (maybeDecl.isPresent() && maybeDecl.get() instanceof FieldParameterDeclaration fpd) { - Field field = fpd.getField(); - throw new org.junit.jupiter.api.extension.ParameterResolutionException( - ("Not enough arguments for @ParameterizedClass field injection: " - + "field '%s' (index %d, type %s) cannot be injected because " - + "only %d argument(s) were provided; at least %d are required.").formatted(field.getName(), - missingIndex, field.getType().getName(), provided, required)); - } - throw new org.junit.jupiter.api.extension.ParameterResolutionException( - "Not enough arguments for @ParameterizedClass field injection: " - + "index %d cannot be injected because only %d argument(s) were provided; at least %d are required.".formatted( - missingIndex, provided, required)); - } - } - private interface Resolver { @Nullable @@ -532,7 +505,6 @@ private record Converter(ArgumentConverter argumentConverter) implements Resolve @Override public @Nullable Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, int invocationIndex) { - ResolverFacade.assertFieldAvailableOrThrow(fieldContext, arguments); Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex()); try { @@ -791,4 +763,7 @@ public boolean supports(ParameterContext parameterContext) { invocationIndex, Optional.of(parameterContext))); } } + + record RequiredParameterCount(int value, String reason) { + } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java index 20c7bca7b60c..9314400d1ac6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -557,14 +557,11 @@ void declaredIndexMustBeUnique() { void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() { var results = executeTestsForClass(NotEnoughArgumentsForFieldsTestCase.class); - results.containerEvents().assertThatEvents() - .haveExactly(1, event(finishedWithFailure( - instanceOf(org.junit.jupiter.api.extension.ParameterResolutionException.class), - message(it -> it.contains("field 's'") - && it.contains("index 1") - && it.contains("only 1 argument") - && it.contains("at least 2")) - ))); + results.containerEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + """ + Configuration error: @ParameterizedClass has 2 required parameters (due to field injection) but there was 1 argument provided. + Note: the provided arguments were [1]"""))); } } From f516e0eba09653dde7101ebc02467e3f1ffa3e92 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 27 Oct 2025 12:18:30 +0100 Subject: [PATCH 4/6] Fix test on Windows --- .../ParameterizedClassIntegrationTests.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java index 9314400d1ac6..5c6b05dcf7a3 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -57,6 +57,7 @@ import java.util.stream.Stream; import org.assertj.core.api.Condition; +import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -558,10 +559,10 @@ void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() { var results = executeTestsForClass(NotEnoughArgumentsForFieldsTestCase.class); results.containerEvents().assertThatEvents() // - .haveExactly(1, finishedWithFailure(message( + .haveExactly(1, finishedWithFailure(message(withPlatformSpecificLineSeparator( """ Configuration error: @ParameterizedClass has 2 required parameters (due to field injection) but there was 1 argument provided. - Note: the provided arguments were [1]"""))); + Note: the provided arguments were [1]""")))); } } @@ -737,12 +738,12 @@ void failsForLifecycleMethodWithInvalidParameters() { var results = executeTestsForClass(LifecycleMethodWithInvalidParametersTestCase.class); - var expectedMessage = """ - 2 configuration errors: - - parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \ - expected type 'int' but found 'long' - - parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith"""// - .replace("\n", System.lineSeparator()); // use platform-specific line separators + var expectedMessage = withPlatformSpecificLineSeparator( + """ + 2 configuration errors: + - parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \ + expected type 'int' but found 'long' + - parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith"""); var failedResult = getFirstTestExecutionResult(results.containerEvents().failed()); assertThat(failedResult.getThrowable().orElseThrow()) // @@ -805,6 +806,10 @@ void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() { } } + private static @NotNull String withPlatformSpecificLineSeparator(String textBlock) { + return textBlock.replace("\n", System.lineSeparator()); + } + // ------------------------------------------------------------------- private static Stream invocationDisplayNames(EngineExecutionResults results) { From 93e167702cf4e54ebc138697a29ede90be5fba42 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 27 Oct 2025 12:19:57 +0100 Subject: [PATCH 5/6] Add to release notes --- .../src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc index 6e7ced558c02..75c135d8c948 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc @@ -54,6 +54,8 @@ repository on GitHub. * The `@CsvSource` and `@CsvFileSource` annotations now allow specifying a custom comment character using the new `commentCharacter` attribute. +* Improve error message when using `@ParameterizedClass` with field injection and not + providing enough arguments. [[release-notes-6.0.1-junit-vintage]] From b57bd29f38831cea81da2e9eca830ec3795e1f88 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 27 Oct 2025 12:21:48 +0100 Subject: [PATCH 6/6] Remove JetBrains annotation --- .../jupiter/params/ParameterizedClassIntegrationTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java index 5c6b05dcf7a3..df50b84e4262 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -57,7 +57,6 @@ import java.util.stream.Stream; import org.assertj.core.api.Condition; -import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -806,7 +805,7 @@ void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() { } } - private static @NotNull String withPlatformSpecificLineSeparator(String textBlock) { + private static String withPlatformSpecificLineSeparator(String textBlock) { return textBlock.replace("\n", System.lineSeparator()); }