Skip to content

Commit 2b42de8

Browse files
Issue: #3708 add ParameterizedTest#argumentCountValidation
This allows parameterized tests to fail when there are more arguments provided than declared by the test method. This is done in a backwards compatible way by only enabling that validation when the new `junit.jupiter.params.argumentCountValidation` is set to `strict` or `ParameterizedTest#argumentCountValidation` is set to `ArgumentCountValidationMode.STRICT`.
1 parent d64e699 commit 2b42de8

File tree

4 files changed

+166
-0
lines changed

4 files changed

+166
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.params;
12+
13+
import org.apiguardian.api.API;
14+
import org.junit.jupiter.params.provider.ArgumentsSource;
15+
16+
/**
17+
* Enumeration of argument count validation modes for {@link ParameterizedTest @ParameterizedTest}.
18+
*
19+
* <p>When an {@link ArgumentsSource} provides more arguments than declared by the test method,
20+
* there might be a bug in the test method or the {@link ArgumentsSource}.
21+
* By default, the additional arguments are ignored.
22+
* {@link ArgumentCountValidationMode} allows you to control how additional arguments are handled.
23+
*
24+
* @since 5.12
25+
* @see ParameterizedTest
26+
*/
27+
@API(status = API.Status.EXPERIMENTAL, since = "5.12")
28+
public enum ArgumentCountValidationMode {
29+
/**
30+
* Use the default cleanup mode.
31+
*
32+
* <p>The default cleanup mode may be changed via the
33+
* {@value ParameterizedTestExtension#ARGUMENT_COUNT_VALIDATION_KEY} configuration parameter
34+
* (see the User Guide for details on configuration parameters).
35+
*/
36+
DEFAULT,
37+
38+
/**
39+
* Use the "none" argument count validation mode.
40+
*
41+
* <p>When there are more arguments provided than declared by the test method,
42+
* these additional arguments are ignored.
43+
*/
44+
NONE,
45+
46+
/**
47+
* Use the strict argument count validation mode.
48+
*
49+
* <p>When there are more arguments provided than declared by the test method, this raises an error.
50+
*/
51+
STRICT,
52+
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apiguardian.api.API;
2323
import org.junit.jupiter.api.TestTemplate;
2424
import org.junit.jupiter.api.extension.ExtendWith;
25+
import org.junit.jupiter.params.provider.ArgumentsSource;
2526

2627
/**
2728
* {@code @ParameterizedTest} is used to signal that the annotated method is a
@@ -291,4 +292,21 @@
291292
@API(status = STABLE, since = "5.10")
292293
boolean autoCloseArguments() default true;
293294

295+
/**
296+
* Configure how the number of arguments provided by an {@link ArgumentsSource} are validated.
297+
*
298+
* <p>Defaults to {@link ArgumentCountValidationMode#DEFAULT}.
299+
*
300+
* <p>When an {@link ArgumentsSource} provides more arguments than declared by the test method,
301+
* there might be a bug in the test method or the {@link ArgumentsSource}.
302+
* By default, the additional arguments are ignored.
303+
* {@code argumentCountValidation} allows you to control how additional arguments are handled.
304+
* This can also be controlled via the {@value ParameterizedTestExtension#ARGUMENT_COUNT_VALIDATION_KEY}
305+
* configuration parameter (see the User Guide for details on configuration parameters).
306+
*
307+
* @since 5.12
308+
* @see ArgumentCountValidationMode
309+
*/
310+
@API(status = EXPERIMENTAL, since = "5.12")
311+
ArgumentCountValidationMode argumentCountValidation() default ArgumentCountValidationMode.DEFAULT;
294312
}

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated;
1616

1717
import java.lang.reflect.Method;
18+
import java.util.Arrays;
19+
import java.util.NoSuchElementException;
20+
import java.util.Optional;
1821
import java.util.concurrent.atomic.AtomicLong;
1922
import java.util.stream.Stream;
2023

24+
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
2125
import org.junit.jupiter.api.extension.ExtensionContext;
2226
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
2327
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
@@ -26,6 +30,8 @@
2630
import org.junit.jupiter.params.provider.ArgumentsProvider;
2731
import org.junit.jupiter.params.provider.ArgumentsSource;
2832
import org.junit.jupiter.params.support.AnnotationConsumerInitializer;
33+
import org.junit.platform.commons.logging.Logger;
34+
import org.junit.platform.commons.logging.LoggerFactory;
2935
import org.junit.platform.commons.util.ExceptionUtils;
3036
import org.junit.platform.commons.util.Preconditions;
3137

@@ -34,10 +40,13 @@
3440
*/
3541
class ParameterizedTestExtension implements TestTemplateInvocationContextProvider {
3642

43+
private static final Logger logger = LoggerFactory.getLogger(ParameterizedTestExtension.class);
44+
3745
private static final String METHOD_CONTEXT_KEY = "context";
3846
static final String ARGUMENT_MAX_LENGTH_KEY = "junit.jupiter.params.displayname.argument.maxlength";
3947
static final String DEFAULT_DISPLAY_NAME = "{default_display_name}";
4048
static final String DISPLAY_NAME_PATTERN_KEY = "junit.jupiter.params.displayname.default";
49+
static final String ARGUMENT_COUNT_VALIDATION_KEY = "junit.jupiter.params.argumentCountValidation";
4150

4251
@Override
4352
public boolean supportsTestTemplate(ExtensionContext context) {
@@ -86,6 +95,7 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
8695
.map(provider -> AnnotationConsumerInitializer.initialize(templateMethod, provider))
8796
.flatMap(provider -> arguments(provider, extensionContext))
8897
.map(arguments -> {
98+
validateArgumentCount(extensionContext, arguments);
8999
invocationCount.incrementAndGet();
90100
return createInvocationContext(formatter, methodContext, arguments, invocationCount.intValue());
91101
})
@@ -99,6 +109,55 @@ private ExtensionContext.Store getStore(ExtensionContext context) {
99109
return context.getStore(Namespace.create(ParameterizedTestExtension.class, context.getRequiredTestMethod()));
100110
}
101111

112+
private void validateArgumentCount(ExtensionContext extensionContext, Arguments arguments) {
113+
ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext);
114+
switch (argumentCountValidationMode) {
115+
case DEFAULT:
116+
case NONE:
117+
return;
118+
case STRICT:
119+
int testParamCount = extensionContext.getRequiredTestMethod().getParameterCount();
120+
int argumentsCount = arguments.get().length;
121+
Preconditions.condition(testParamCount == argumentsCount, () -> String.format(
122+
"Configuration error: the @ParameterizedTest has %s argument(s) but there were %s argument(s) provided./nNote: the provided arguments are %s",
123+
testParamCount, argumentsCount, Arrays.toString(arguments.get())));
124+
break;
125+
default:
126+
throw new ExtensionConfigurationException(
127+
"Unsupported argument count validation mode: " + argumentCountValidationMode);
128+
}
129+
}
130+
131+
private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) {
132+
ParameterizedTest parameterizedTest = findAnnotation(//
133+
extensionContext.getRequiredTestMethod(), ParameterizedTest.class//
134+
).orElseThrow(NoSuchElementException::new);
135+
if (parameterizedTest.argumentCountValidation() != ArgumentCountValidationMode.DEFAULT) {
136+
return parameterizedTest.argumentCountValidation();
137+
}
138+
else {
139+
return getArgumentCountValidationModeConfiguration(extensionContext);
140+
}
141+
}
142+
143+
private ArgumentCountValidationMode getArgumentCountValidationModeConfiguration(ExtensionContext extensionContext) {
144+
String key = ARGUMENT_COUNT_VALIDATION_KEY;
145+
ArgumentCountValidationMode fallback = ArgumentCountValidationMode.DEFAULT;
146+
Optional<String> optionalValue = extensionContext.getConfigurationParameter(key);
147+
if (optionalValue.isPresent()) {
148+
String value = optionalValue.get();
149+
return Arrays.stream(ArgumentCountValidationMode.values()).filter(
150+
mode -> mode.name().equalsIgnoreCase(value)).findFirst().orElseGet(() -> {
151+
logger.warn(() -> String.format(
152+
"Ignored invalid configuration '%s' set via the '%s' configuration parameter.", value, key));
153+
return fallback;
154+
});
155+
}
156+
else {
157+
return fallback;
158+
}
159+
}
160+
102161
private TestTemplateInvocationContext createInvocationContext(ParameterizedTestNameFormatter formatter,
103162
ParameterizedTestMethodContext methodContext, Arguments arguments, int invocationIndex) {
104163

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
import org.junit.platform.testkit.engine.EngineExecutionResults;
116116
import org.junit.platform.testkit.engine.EngineTestKit;
117117
import org.junit.platform.testkit.engine.Event;
118+
import org.junit.platform.testkit.engine.EventConditions;
118119
import org.opentest4j.TestAbortedException;
119120

120121
/**
@@ -1093,6 +1094,42 @@ private EngineExecutionResults execute(String methodName, Class<?>... methodPara
10931094

10941095
}
10951096

1097+
@Nested
1098+
class UnusedArgumentsWithStrictArgumentsCountIntegrationTests {
1099+
@Test
1100+
void failsWithArgumentsSourceProvidingUnusedArguments() {
1101+
var results = execute(UnusedArgumentsTestCase.class, "testWithTwoUnusedStringArgumentsProvider",
1102+
String.class);
1103+
results.allEvents().assertThatEvents() //
1104+
.haveExactly(1, event(EventConditions.finishedWithFailure(message(
1105+
"Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided./nNote: the provided arguments are [foo, unused1]"))));
1106+
}
1107+
1108+
@Test
1109+
void failsWithMethodSourceProvidingUnusedArguments() {
1110+
var results = execute(UnusedArgumentsTestCase.class, "testWithMethodSourceProvidingUnusedArguments",
1111+
String.class);
1112+
results.allEvents().assertThatEvents() //
1113+
.haveExactly(1, event(EventConditions.finishedWithFailure(message(
1114+
"Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided./nNote: the provided arguments are [foo, unused1]"))));
1115+
}
1116+
1117+
@Test
1118+
void executesWithMethodSourceProvidingUnusedArguments() {
1119+
var results = execute(RepeatableSourcesTestCase.class, "testWithRepeatableCsvSource", String.class);
1120+
results.allEvents().assertThatEvents() //
1121+
.haveExactly(1, event(test(), displayName("[1] argument=a"), finishedWithFailure(message("a")))) //
1122+
.haveExactly(1, event(test(), displayName("[2] argument=b"), finishedWithFailure(message("b"))));
1123+
}
1124+
1125+
private EngineExecutionResults execute(Class<?> javaClass, String methodName,
1126+
Class<?>... methodParameterTypes) {
1127+
return EngineTestKit.engine(new JupiterTestEngine()).selectors(
1128+
selectMethod(javaClass, methodName, methodParameterTypes)).configurationParameter(
1129+
ParameterizedTestExtension.ARGUMENT_COUNT_VALIDATION_KEY, "strict").execute();
1130+
}
1131+
}
1132+
10961133
@Nested
10971134
class RepeatableSourcesIntegrationTests {
10981135

0 commit comments

Comments
 (0)