Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ParameterizedTest#argumentCountValidation #4045

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2e56d56
Issue: #3708 add ParameterizedTest#argumentCountValidation
jonas-jebing-at-ebay Oct 4, 2024
3d40ebe
Merge branch 'main' into argument-count-validation-mode
JonasJebing Oct 23, 2024
98cdf69
address PR comments
JonasJebing Oct 23, 2024
89a7443
gradle spotlessApply
JonasJebing Oct 23, 2024
5c71632
move example to .adoc because it's a test that should error
JonasJebing Oct 23, 2024
edd9f46
fix newline char assertion for Windows
JonasJebing Oct 23, 2024
beb53ab
gradle spotlessApply
JonasJebing Oct 23, 2024
b117b69
Merge branch 'main' into argument-count-validation-mode
JonasJebing Oct 23, 2024
37216f8
Merge branch 'main' into argument-count-validation-mode
JonasJebing Oct 24, 2024
f87404f
add change to release-notes 5.12.0-M1
JonasJebing Oct 24, 2024
14a071b
fix ArgumentCountValidationMode javadoc typo
JonasJebing Oct 24, 2024
762c39a
improve ParameterizedTest javadoc
JonasJebing Oct 24, 2024
8a84dc2
use underscores for unused lambda parameter
JonasJebing Oct 24, 2024
09ced82
use root context store for caching config value
JonasJebing Oct 24, 2024
0bf0b05
remove mention of experimental status from release note
JonasJebing Oct 24, 2024
f189f07
Merge branch 'main' into argument-count-validation-mode
JonasJebing Oct 25, 2024
36e7729
`@ParameterizedTest`s to Parameterized tests
JonasJebing Oct 25, 2024
851a4db
improve release note
JonasJebing Oct 25, 2024
a986bba
move user guide example to ParameterizedTestDemo
JonasJebing Oct 25, 2024
2dafffb
Merge branch 'main' into argument-count-validation-mode
JonasJebing Nov 12, 2024
e70240c
move argument count validation to happen later
JonasJebing Nov 12, 2024
3d84b9f
update javadoc to use new ArgumentCountValidator
JonasJebing Nov 12, 2024
dd6b55a
retrigger checks
JonasJebing Nov 13, 2024
1151f6b
Merge branch 'main' into argument-count-validation-mode
JonasJebing Nov 14, 2024
906881c
add small ArgumentCountValidator perf optimisations
JonasJebing Nov 14, 2024
89ceaec
Merge branch 'main' into argument-count-validation-mode
JonasJebing Nov 15, 2024
9619c36
Merge branch 'main' into argument-count-validation-mode
JonasJebing Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ JUnit repository on GitHub.
a test-scoped `ExtensionContext` in `Extension` methods called during test class
instantiation. This behavior will become the default in future versions of JUnit.
* `@TempDir` is now supported on test class constructors.
* Parameterized tests now support argument count validation.
If the `junit.jupiter.params.argumentCountValidation=strict` configuration parameter
or the `@ParameterizedTest(argumentCountValidation = STRICT)` attribute is set, any
mismatch between the declared number of arguments and the number of arguments provided
by the arguments source will result in an error. By default, it's still only an error if
there are fewer arguments provided than declared.
* The new `PreInterruptCallback` extension point defines the API for `Extensions` that
wish to be called prior to invocations of `Thread#interrupt()` by the `@Timeout`
extension.
Expand Down
23 changes: 23 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2020,6 +2020,29 @@ The following annotations are repeatable:
* `@CsvFileSource`
* `@ArgumentsSource`

[[writing-tests-parameterized-tests-argument-count-validation]]
==== Argument Count Validation

WARNING: Argument count validation is currently an _experimental_ feature. You're invited to
give it a try and provide feedback to the JUnit team so they can improve and eventually
<<api-evolution, promote>> this feature.

By default, when an arguments source provides more arguments than the test method needs,
those additional arguments are ignored and the test executes as usual.
This can lead to bugs where arguments are never passed to the parameterized test method.

To prevent this, you can set argument count validation to 'strict'.
Then, any additional arguments will cause an error instead.

To change this behavior for all tests, set the `junit.jupiter.params.argumentCountValidation`
<<running-tests-config-params, configuration parameter>> to `strict`.
To change this behavior for a single test,
use the `argumentCountValidation` attribute of the `@ParameterizedTest` annotation:

[source,java,indent=0]
----
include::{testDir}/example/ParameterizedTestDemo.java[tags=argument_count_validation]
----

[[writing-tests-parameterized-tests-argument-conversion]]
==== Argument Conversion
Expand Down
10 changes: 10 additions & 0 deletions documentation/src/test/java/example/ParameterizedTestDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.params.ArgumentCountValidationMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.aggregator.AggregateWith;
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
Expand Down Expand Up @@ -607,4 +608,13 @@ static Stream<String> otherProvider() {
return Stream.of("bar");
}
// end::repeatable_annotations[]

@extensions.ExpectToFail
// tag::argument_count_validation[]
@ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.STRICT)
@CsvSource({ "42, -666" })
void testWithArgumentCountValidation(int number) {
assertTrue(number > 0);
}
// end::argument_count_validation[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.params;

import org.apiguardian.api.API;
import org.junit.jupiter.params.provider.ArgumentsSource;

/**
* Enumeration of argument count validation modes for {@link ParameterizedTest @ParameterizedTest}.
*
* <p>When an {@link ArgumentsSource} provides more arguments than declared by the test method,
* there might be a bug in the test method or the {@link ArgumentsSource}.
* By default, the additional arguments are ignored.
* {@link ArgumentCountValidationMode} allows you to control how additional arguments are handled.
*
* @since 5.12
* @see ParameterizedTest
*/
@API(status = API.Status.EXPERIMENTAL, since = "5.12")
public enum ArgumentCountValidationMode {
/**
* Use the default validation mode.
*
* <p>The default validation mode may be changed via the
* {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} configuration parameter
* (see the User Guide for details on configuration parameters).
*/
DEFAULT,

/**
* Use the "none" argument count validation mode.
*
* <p>When there are more arguments provided than declared by the test method,
* these additional arguments are ignored.
*/
NONE,

/**
* Use the strict argument count validation mode.
*
* <p>When there are more arguments provided than declared by the test method, this raises an error.
*/
STRICT,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.params;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;

import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.Preconditions;

class ArgumentCountValidator implements InvocationInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ArgumentCountValidator.class);

static final String ARGUMENT_COUNT_VALIDATION_KEY = "junit.jupiter.params.argumentCountValidation";
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(
ArgumentCountValidator.class);

private final ParameterizedTestMethodContext methodContext;
private final Arguments arguments;

ArgumentCountValidator(ParameterizedTestMethodContext methodContext, Arguments arguments) {
this.methodContext = methodContext;
this.arguments = arguments;
}

@Override
public void interceptTestTemplateMethod(InvocationInterceptor.Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
validateArgumentCount(extensionContext, arguments);
invocation.proceed();
}

private ExtensionContext.Store getStore(ExtensionContext context) {
return context.getRoot().getStore(NAMESPACE);
}

private void validateArgumentCount(ExtensionContext extensionContext, Arguments arguments) {
ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext);
switch (argumentCountValidationMode) {
case DEFAULT:
case NONE:
return;
case STRICT:
int testParamCount = extensionContext.getRequiredTestMethod().getParameterCount();
int argumentsCount = arguments.get().length;
Preconditions.condition(testParamCount == argumentsCount, () -> String.format(
"Configuration error: the @ParameterizedTest has %s argument(s) but there were %s argument(s) provided.%nNote: the provided arguments are %s",
testParamCount, argumentsCount, Arrays.toString(arguments.get())));
break;
default:
throw new ExtensionConfigurationException(
"Unsupported argument count validation mode: " + argumentCountValidationMode);
}
}

private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) {
ParameterizedTest parameterizedTest = methodContext.annotation;
if (parameterizedTest.argumentCountValidation() != ArgumentCountValidationMode.DEFAULT) {
return parameterizedTest.argumentCountValidation();
}
else {
return getArgumentCountValidationModeConfiguration(extensionContext);
}
}

private ArgumentCountValidationMode getArgumentCountValidationModeConfiguration(ExtensionContext extensionContext) {
String key = ARGUMENT_COUNT_VALIDATION_KEY;
ArgumentCountValidationMode fallback = ArgumentCountValidationMode.NONE;
ExtensionContext.Store store = getStore(extensionContext);
return store.getOrComputeIfAbsent(key, __ -> {
Optional<String> optionalConfigValue = extensionContext.getConfigurationParameter(key);
if (optionalConfigValue.isPresent()) {
String configValue = optionalConfigValue.get();
Optional<ArgumentCountValidationMode> enumValue = Arrays.stream(
ArgumentCountValidationMode.values()).filter(
mode -> mode.name().equalsIgnoreCase(configValue)).findFirst();
if (enumValue.isPresent()) {
logger.config(() -> String.format(
"Using ArgumentCountValidationMode '%s' set via the '%s' configuration parameter.",
enumValue.get().name(), key));
return enumValue.get();
}
else {
logger.warn(() -> String.format(
"Invalid ArgumentCountValidationMode '%s' set via the '%s' configuration parameter. "
+ "Falling back to the %s default value.",
configValue, key, fallback.name()));
return fallback;
}
}
else {
return fallback;
}
}, ArgumentCountValidationMode.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apiguardian.api.API;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.provider.ArgumentsSource;

/**
* {@code @ParameterizedTest} is used to signal that the annotated method is a
Expand Down Expand Up @@ -305,4 +306,21 @@
@API(status = EXPERIMENTAL, since = "5.12")
boolean requireArguments() default true;

/**
* Configure how the number of arguments provided by an {@link ArgumentsSource} are validated.
*
* <p>Defaults to {@link ArgumentCountValidationMode#DEFAULT}.
*
* <p>When an {@link ArgumentsSource} provides more arguments than declared by the test method,
* there might be a bug in the test method or the {@link ArgumentsSource}.
* By default, the additional arguments are ignored.
* {@code argumentCountValidation} allows you to control how additional arguments are handled.
* The default can be configured via the {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY}
* configuration parameter (see the User Guide for details on configuration parameters).
*
* @since 5.12
* @see ArgumentCountValidationMode
*/
@API(status = EXPERIMENTAL, since = "5.12")
ArgumentCountValidationMode argumentCountValidation() default ArgumentCountValidationMode.DEFAULT;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

package org.junit.jupiter.params;

import static java.util.Collections.singletonList;

import java.util.Arrays;
import java.util.List;

Expand Down Expand Up @@ -47,8 +45,9 @@ public String getDisplayName(int invocationIndex) {

@Override
public List<Extension> getAdditionalExtensions() {
return singletonList(
new ParameterizedTestParameterResolver(this.methodContext, this.consumedArguments, this.invocationIndex));
return Arrays.asList(
new ParameterizedTestParameterResolver(this.methodContext, this.consumedArguments, this.invocationIndex),
new ArgumentCountValidator(this.methodContext, this.arguments));
}

private static Object[] consumedArguments(ParameterizedTestMethodContext methodContext, Object[] arguments) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
import org.junit.platform.testkit.engine.EngineExecutionResults;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.platform.testkit.engine.Event;
import org.junit.platform.testkit.engine.EventConditions;
import org.opentest4j.TestAbortedException;

/**
Expand Down Expand Up @@ -1112,6 +1113,74 @@ private EngineExecutionResults execute(String methodName, Class<?>... methodPara

}

@Nested
class UnusedArgumentsWithStrictArgumentsCountIntegrationTests {
@Test
void failsWithArgumentsSourceProvidingUnusedArguments() {
var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class,
"testWithTwoUnusedStringArgumentsProvider", String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format(
"Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]")))));
}

@Test
void failsWithMethodSourceProvidingUnusedArguments() {
var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class,
"testWithMethodSourceProvidingUnusedArguments", String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format(
"Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]")))));
}

@Test
void failsWithCsvSourceUnusedArgumentsAndStrictArgumentCountValidationAnnotationAttribute() {
var results = execute(ArgumentCountValidationMode.NONE, UnusedArgumentsTestCase.class,
"testWithStrictArgumentCountValidation", String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format(
"Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]")))));
}

@Test
void failsWithCsvSourceUnusedArgumentsButExecutesRemainingArgumentsWhereThereIsNoUnusedArgument() {
var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class,
"testWithCsvSourceContainingDifferentNumbersOfArguments", String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format(
"Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))) //
.haveExactly(1,
event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar"))));
}

@Test
void executesWithCsvSourceUnusedArgumentsAndArgumentCountValidationAnnotationAttribute() {
var results = execute(ArgumentCountValidationMode.NONE, UnusedArgumentsTestCase.class,
"testWithNoneArgumentCountValidation", String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1,
event(test(), displayName("[1] argument=foo"), finishedWithFailure(message("foo"))));
}

@Test
void executesWithMethodSourceProvidingUnusedArguments() {
var results = execute(ArgumentCountValidationMode.STRICT, RepeatableSourcesTestCase.class,
"testWithRepeatableCsvSource", String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1, event(test(), displayName("[1] argument=a"), finishedWithFailure(message("a")))) //
.haveExactly(1, event(test(), displayName("[2] argument=b"), finishedWithFailure(message("b"))));
}

private EngineExecutionResults execute(ArgumentCountValidationMode configurationValue, Class<?> javaClass,
String methodName, Class<?>... methodParameterTypes) {
return EngineTestKit.engine(new JupiterTestEngine()) //
.selectors(selectMethod(javaClass, methodName, methodParameterTypes)) //
.configurationParameter(ArgumentCountValidator.ARGUMENT_COUNT_VALIDATION_KEY,
configurationValue.name().toLowerCase()) //
.execute();
}
}

marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
@Nested
class RepeatableSourcesIntegrationTests {

Expand Down Expand Up @@ -2028,6 +2097,23 @@ void testWithFieldSourceProvidingUnusedArguments(String argument) {
static Supplier<Stream<Arguments>> unusedArgumentsProviderField = //
() -> Stream.of(arguments("foo", "unused1"), arguments("bar", "unused2"));

@ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.STRICT)
@CsvSource({ "foo, unused1" })
void testWithStrictArgumentCountValidation(String argument) {
fail(argument);
}

@ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.NONE)
@CsvSource({ "foo, unused1" })
void testWithNoneArgumentCountValidation(String argument) {
fail(argument);
}

@ParameterizedTest
@CsvSource({ "foo, unused1", "bar" })
void testWithCsvSourceContainingDifferentNumbersOfArguments(String argument) {
fail(argument);
}
}

static class LifecycleTestCase {
Expand Down