Skip to content

Commit c99fba6

Browse files
Validate that there are enough arguments to inject @Parameter fields
`ArgumentCountValidator` now also validates that there are enough arguments for all required parameters which is currently only the case for indexed `@Parameter` fields. Fixes #5079. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent debc12f commit c99fba6

File tree

5 files changed

+81
-14
lines changed

5 files changed

+81
-14
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.0.1.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ repository on GitHub.
5656

5757
* The `@CsvSource` and `@CsvFileSource` annotations now allow specifying
5858
a custom comment character using the new `commentCharacter` attribute.
59+
* Improve error message when using `@ParameterizedClass` with field injection and not
60+
providing enough arguments.
5961

6062

6163
[[release-notes-6.0.1-junit-vintage]]

junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.Arrays;
1414
import java.util.Optional;
1515

16+
import org.jspecify.annotations.Nullable;
1617
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
1718
import org.junit.jupiter.api.extension.ExtensionContext;
1819
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
@@ -36,6 +37,7 @@ class ArgumentCountValidator {
3637
}
3738

3839
void validate(ExtensionContext extensionContext) {
40+
validateRequiredArgumentsArePresent();
3941
ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext);
4042
switch (argumentCountValidationMode) {
4143
case DEFAULT, NONE -> {
@@ -45,17 +47,34 @@ void validate(ExtensionContext extensionContext) {
4547
this.arguments);
4648
int totalCount = this.arguments.getTotalLength();
4749
Preconditions.condition(consumedCount == totalCount,
48-
() -> "Configuration error: @%s consumes %s %s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted(
49-
this.declarationContext.getAnnotationName(), consumedCount,
50-
pluralize(consumedCount, "parameter", "parameters"), pluralize(totalCount, "was", "were"),
51-
totalCount, pluralize(totalCount, "argument", "arguments"),
52-
Arrays.toString(this.arguments.getAllPayloads())));
50+
() -> wrongNumberOfArgumentsMessages("consumes", consumedCount, null, null));
5351
}
5452
default -> throw new ExtensionConfigurationException(
5553
"Unsupported argument count validation mode: " + argumentCountValidationMode);
5654
}
5755
}
5856

57+
private void validateRequiredArgumentsArePresent() {
58+
var requiredParameterCount = this.declarationContext.getResolverFacade().getRequiredParameterCount();
59+
if (requiredParameterCount != null) {
60+
var totalCount = this.arguments.getTotalLength();
61+
Preconditions.condition(requiredParameterCount.value() <= totalCount,
62+
() -> wrongNumberOfArgumentsMessages("has", requiredParameterCount.value(), "required",
63+
requiredParameterCount.reason()));
64+
}
65+
}
66+
67+
private String wrongNumberOfArgumentsMessages(String verb, int actualCount, @Nullable String parameterAdjective,
68+
@Nullable String reason) {
69+
int totalCount = this.arguments.getTotalLength();
70+
return "Configuration error: @%s %s %s %s%s%s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted(
71+
this.declarationContext.getAnnotationName(), verb, actualCount,
72+
parameterAdjective == null ? "" : parameterAdjective + " ",
73+
pluralize(actualCount, "parameter", "parameters"), reason == null ? "" : " (due to %s)".formatted(reason),
74+
pluralize(totalCount, "was", "were"), totalCount, pluralize(totalCount, "argument", "arguments"),
75+
Arrays.toString(this.arguments.getAllPayloads()));
76+
}
77+
5978
private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) {
6079
ArgumentCountValidationMode mode = declarationContext.getArgumentCountValidationMode();
6180
if (mode != ArgumentCountValidationMode.DEFAULT) {

junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public ResolverFacade getResolverFacade() {
130130
@Override
131131
public ParameterizedClassInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter,
132132
Arguments arguments, int invocationIndex) {
133+
133134
return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex);
134135
}
135136

junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ static ResolverFacade create(Class<?> clazz, List<Field> fields) {
9898
Stream.concat(uniqueIndexedParameters.values().stream(), aggregatorParameters.stream()) //
9999
.forEach(declaration -> makeAccessible(declaration.getField()));
100100

101-
return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0);
101+
var requiredParameterCount = new RequiredParameterCount(uniqueIndexedParameters.size(), "field injection");
102+
103+
return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0, requiredParameterCount);
102104
}
103105

104106
static ResolverFacade create(Constructor<?> constructor, ParameterizedClass annotation) {
@@ -155,27 +157,35 @@ else if (aggregatorParameters.isEmpty()) {
155157
}
156158
}
157159
return new ResolverFacade(executable, indexedParameters, new LinkedHashSet<>(aggregatorParameters.values()),
158-
indexOffset);
160+
indexOffset, null);
159161
}
160162

161163
private final int parameterIndexOffset;
162164
private final Map<ParameterDeclaration, Resolver> resolvers;
163165
private final DefaultParameterDeclarations indexedParameterDeclarations;
164166
private final Set<? extends ResolvableParameterDeclaration> aggregatorParameters;
167+
private final @Nullable RequiredParameterCount requiredParameterCount;
165168

166169
private ResolverFacade(AnnotatedElement sourceElement,
167170
NavigableMap<Integer, ? extends ResolvableParameterDeclaration> indexedParameters,
168-
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset) {
171+
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset,
172+
@Nullable RequiredParameterCount requiredParameterCount) {
169173
this.aggregatorParameters = aggregatorParameters;
170174
this.parameterIndexOffset = parameterIndexOffset;
171175
this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size());
172176
this.indexedParameterDeclarations = new DefaultParameterDeclarations(sourceElement, indexedParameters);
177+
this.requiredParameterCount = requiredParameterCount;
173178
}
174179

175180
ParameterDeclarations getIndexedParameterDeclarations() {
176181
return this.indexedParameterDeclarations;
177182
}
178183

184+
@Nullable
185+
RequiredParameterCount getRequiredParameterCount() {
186+
return this.requiredParameterCount;
187+
}
188+
179189
boolean isSupportedParameter(ParameterContext parameterContext, EvaluatedArgumentSet arguments) {
180190
int index = toLogicalIndex(parameterContext);
181191
if (this.indexedParameterDeclarations.get(index).isPresent()) {
@@ -495,6 +505,7 @@ private record Converter(ArgumentConverter argumentConverter) implements Resolve
495505
@Override
496506
public @Nullable Object resolve(FieldContext fieldContext, ExtensionContext extensionContext,
497507
EvaluatedArgumentSet arguments, int invocationIndex) {
508+
498509
Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex());
499510
try {
500511
return this.argumentConverter.convert(argument, fieldContext);
@@ -752,4 +763,7 @@ public boolean supports(ParameterContext parameterContext) {
752763
invocationIndex, Optional.of(parameterContext)));
753764
}
754765
}
766+
767+
record RequiredParameterCount(int value, String reason) {
768+
}
755769
}

jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,17 @@ void declaredIndexMustBeUnique() {
552552
"Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted(
553553
classTemplateClass.getName(), classTemplateClass.getName()))));
554554
}
555+
556+
@Test
557+
void failsWithMeaningfulErrorWhenTooFewArgumentsProvidedForFieldInjection() {
558+
var results = executeTestsForClass(NotEnoughArgumentsForFieldsTestCase.class);
559+
560+
results.containerEvents().assertThatEvents() //
561+
.haveExactly(1, finishedWithFailure(message(withPlatformSpecificLineSeparator(
562+
"""
563+
Configuration error: @ParameterizedClass has 2 required parameters (due to field injection) but there was 1 argument provided.
564+
Note: the provided arguments were [1]"""))));
565+
}
555566
}
556567

557568
@Nested
@@ -726,12 +737,12 @@ void failsForLifecycleMethodWithInvalidParameters() {
726737

727738
var results = executeTestsForClass(LifecycleMethodWithInvalidParametersTestCase.class);
728739

729-
var expectedMessage = """
730-
2 configuration errors:
731-
- parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \
732-
expected type 'int' but found 'long'
733-
- parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith"""//
734-
.replace("\n", System.lineSeparator()); // use platform-specific line separators
740+
var expectedMessage = withPlatformSpecificLineSeparator(
741+
"""
742+
2 configuration errors:
743+
- parameter 'value' with index 0 is incompatible with the parameter declared on the parameterized class: \
744+
expected type 'int' but found 'long'
745+
- parameter 'anotherValue' with index 1 must not be annotated with @ConvertWith""");
735746

736747
var failedResult = getFirstTestExecutionResult(results.containerEvents().failed());
737748
assertThat(failedResult.getThrowable().orElseThrow()) //
@@ -794,6 +805,10 @@ void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() {
794805
}
795806
}
796807

808+
private static String withPlatformSpecificLineSeparator(String textBlock) {
809+
return textBlock.replace("\n", System.lineSeparator());
810+
}
811+
797812
// -------------------------------------------------------------------
798813

799814
private static Stream<String> invocationDisplayNames(EngineExecutionResults results) {
@@ -1693,6 +1708,22 @@ void test() {
16931708
}
16941709
}
16951710

1711+
@ParameterizedClass
1712+
@ValueSource(ints = 1)
1713+
static class NotEnoughArgumentsForFieldsTestCase {
1714+
1715+
@Parameter(0)
1716+
int i;
1717+
1718+
@Parameter(1)
1719+
String s;
1720+
1721+
@org.junit.jupiter.api.Test
1722+
void test() {
1723+
fail("should not be called");
1724+
}
1725+
}
1726+
16961727
@ParameterizedClass
16971728
@CsvSource({ "unused1, foo, unused2, bar", "unused4, baz, unused5, qux" })
16981729
static class InvalidUnusedParameterIndexesTestCase {

0 commit comments

Comments
 (0)