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]] 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/ParameterizedClassContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java index f0402064a5a8..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 @@ -130,6 +130,7 @@ public ResolverFacade getResolverFacade() { @Override public ParameterizedClassInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex) { + return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex); } 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..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()) { @@ -495,6 +505,7 @@ private record Converter(ArgumentConverter argumentConverter) implements Resolve @Override public @Nullable Object resolve(FieldContext fieldContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, int invocationIndex) { + Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex()); try { return this.argumentConverter.convert(argument, fieldContext); @@ -752,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 fea99691a7a9..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 @@ -552,6 +552,17 @@ 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, 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]""")))); + } } @Nested @@ -726,12 +737,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()) // @@ -794,6 +805,10 @@ void lifecycleMethodsMustNotBeDeclaredInRegularTestClasses() { } } + private static String withPlatformSpecificLineSeparator(String textBlock) { + return textBlock.replace("\n", System.lineSeparator()); + } + // ------------------------------------------------------------------- private static Stream invocationDisplayNames(EngineExecutionResults results) { @@ -1693,6 +1708,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 {