Skip to content

Commit 9c0c755

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 <mail@marcphilipp.de>
1 parent 2bdea13 commit 9c0c755

File tree

5 files changed

+85
-21
lines changed

5 files changed

+85
-21
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ repository on GitHub.
4545
[[release-notes-5.14.1-junit-jupiter-new-features-and-improvements]]
4646
==== New Features and Improvements
4747

48-
* ❓
48+
* Improve error message when using `@ParameterizedClass` with field injection and not
49+
providing enough arguments.
4950

5051

5152
[[release-notes-5.14.1-junit-vintage]]

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

Lines changed: 28 additions & 10 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:
@@ -45,19 +47,35 @@ void validate(ExtensionContext extensionContext) {
4547
int consumedCount = this.declarationContext.getResolverFacade().determineConsumedArgumentCount(
4648
this.arguments);
4749
int totalCount = this.arguments.getTotalLength();
48-
Preconditions.condition(consumedCount == totalCount, () -> String.format(
49-
"Configuration error: @%s consumes %s %s but there %s %s %s provided.%nNote: the provided arguments were %s",
50-
this.declarationContext.getAnnotationName(), consumedCount,
51-
pluralize(consumedCount, "parameter", "parameters"), pluralize(totalCount, "was", "were"),
52-
totalCount, pluralize(totalCount, "argument", "arguments"),
53-
Arrays.toString(this.arguments.getAllPayloads())));
54-
break;
55-
default:
56-
throw new ExtensionConfigurationException(
57-
"Unsupported argument count validation mode: " + argumentCountValidationMode);
50+
Preconditions.condition(consumedCount == totalCount,
51+
() -> wrongNumberOfArgumentsMessages("consumes", consumedCount, null, null));
52+
}
53+
default -> throw new ExtensionConfigurationException(
54+
"Unsupported argument count validation mode: " + argumentCountValidationMode);
55+
}
56+
}
57+
58+
private void validateRequiredArgumentsArePresent() {
59+
var requiredParameterCount = this.declarationContext.getResolverFacade().getRequiredParameterCount();
60+
if (requiredParameterCount != null) {
61+
var totalCount = this.arguments.getTotalLength();
62+
Preconditions.condition(requiredParameterCount.value() <= totalCount,
63+
() -> wrongNumberOfArgumentsMessages("has", requiredParameterCount.value(), "required",
64+
requiredParameterCount.reason()));
5865
}
5966
}
6067

68+
private String wrongNumberOfArgumentsMessages(String verb, int actualCount, @Nullable String parameterAdjective,
69+
@Nullable String reason) {
70+
int totalCount = this.arguments.getTotalLength();
71+
return "Configuration error: @%s %s %s %s%s%s but there %s %s %s provided.%nNote: the provided arguments were %s".formatted(
72+
this.declarationContext.getAnnotationName(), verb, actualCount,
73+
parameterAdjective == null ? "" : parameterAdjective + " ",
74+
pluralize(actualCount, "parameter", "parameters"), reason == null ? "" : " (due to %s)".formatted(reason),
75+
pluralize(totalCount, "was", "were"), totalCount, pluralize(totalCount, "argument", "arguments"),
76+
Arrays.toString(this.arguments.getAllPayloads()));
77+
}
78+
6179
private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) {
6280
ArgumentCountValidationMode mode = declarationContext.getArgumentCountValidationMode();
6381
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
@@ -129,6 +129,7 @@ public ResolverFacade getResolverFacade() {
129129
@Override
130130
public ClassTemplateInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter,
131131
Arguments arguments, int invocationIndex) {
132+
132133
return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex);
133134
}
134135

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
@@ -96,7 +96,9 @@ static ResolverFacade create(Class<?> clazz, List<Field> fields) {
9696
Stream.concat(uniqueIndexedParameters.values().stream(), aggregatorParameters.stream()) //
9797
.forEach(declaration -> makeAccessible(declaration.getField()));
9898

99-
return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0);
99+
var requiredParameterCount = new RequiredParameterCount(uniqueIndexedParameters.size(), "field injection");
100+
101+
return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0, requiredParameterCount);
100102
}
101103

102104
static ResolverFacade create(Constructor<?> constructor, ParameterizedClass annotation) {
@@ -146,27 +148,35 @@ else if (aggregatorParameters.isEmpty()) {
146148
}
147149
}
148150
return new ResolverFacade(executable, indexedParameters, new LinkedHashSet<>(aggregatorParameters.values()),
149-
indexOffset);
151+
indexOffset, null);
150152
}
151153

152154
private final int parameterIndexOffset;
153155
private final Map<ParameterDeclaration, Resolver> resolvers;
154156
private final DefaultParameterDeclarations indexedParameterDeclarations;
155157
private final Set<? extends ResolvableParameterDeclaration> aggregatorParameters;
158+
private final @Nullable RequiredParameterCount requiredParameterCount;
156159

157160
private ResolverFacade(AnnotatedElement sourceElement,
158161
NavigableMap<Integer, ? extends ResolvableParameterDeclaration> indexedParameters,
159-
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset) {
162+
Set<? extends ResolvableParameterDeclaration> aggregatorParameters, int parameterIndexOffset,
163+
@Nullable RequiredParameterCount requiredParameterCount) {
160164
this.aggregatorParameters = aggregatorParameters;
161165
this.parameterIndexOffset = parameterIndexOffset;
162166
this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size());
163167
this.indexedParameterDeclarations = new DefaultParameterDeclarations(sourceElement, indexedParameters);
168+
this.requiredParameterCount = requiredParameterCount;
164169
}
165170

166171
ParameterDeclarations getIndexedParameterDeclarations() {
167172
return this.indexedParameterDeclarations;
168173
}
169174

175+
@Nullable
176+
RequiredParameterCount getRequiredParameterCount() {
177+
return this.requiredParameterCount;
178+
}
179+
170180
boolean isSupportedParameter(ParameterContext parameterContext, EvaluatedArgumentSet arguments) {
171181
int index = toLogicalIndex(parameterContext);
172182
if (this.indexedParameterDeclarations.get(index).isPresent()) {
@@ -491,6 +501,7 @@ public Object resolve(ParameterContext parameterContext, int parameterIndex, Ext
491501
@Override
492502
public Object resolve(FieldContext fieldContext, ExtensionContext extensionContext,
493503
EvaluatedArgumentSet arguments, int invocationIndex) {
504+
494505
Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex());
495506
try {
496507
return this.argumentConverter.convert(argument, fieldContext);
@@ -769,4 +780,7 @@ public Object resolve(ParameterContext parameterContext, ExtensionContext extens
769780
invocationIndex, Optional.of(parameterContext)));
770781
}
771782
}
783+
784+
record RequiredParameterCount(int value, String reason) {
785+
}
772786
}

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

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

554565
@Nested
@@ -723,13 +734,12 @@ void failsForLifecycleMethodWithInvalidParameters() {
723734

724735
var results = executeTestsForClass(LifecycleMethodWithInvalidParametersTestCase.class);
725736

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

734744
var failedResult = getFirstTestExecutionResult(results.containerEvents().failed());
735745
assertThat(failedResult.getThrowable().orElseThrow()) //
@@ -792,6 +802,10 @@ void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() {
792802
}
793803
}
794804

805+
private static String withPlatformSpecificLineSeparator(String textBlock) {
806+
return textBlock.replace("\n", System.lineSeparator());
807+
}
808+
795809
// -------------------------------------------------------------------
796810

797811
private static Stream<String> invocationDisplayNames(EngineExecutionResults results) {
@@ -1689,6 +1703,22 @@ void test() {
16891703
}
16901704
}
16911705

1706+
@ParameterizedClass
1707+
@ValueSource(ints = 1)
1708+
static class NotEnoughArgumentsForFieldsTestCase {
1709+
1710+
@Parameter(0)
1711+
int i;
1712+
1713+
@Parameter(1)
1714+
String s;
1715+
1716+
@org.junit.jupiter.api.Test
1717+
void test() {
1718+
fail("should not be called");
1719+
}
1720+
}
1721+
16921722
@ParameterizedClass
16931723
@CsvSource({ "unused1, foo, unused2, bar", "unused4, baz, unused5, qux" })
16941724
static class InvalidUnusedParameterIndexesTestCase {

0 commit comments

Comments
 (0)