Skip to content

Commit

Permalink
Issue: #3708 add ParameterizedTest#argumentCountValidation
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
jonas-jebing-at-ebay committed Oct 4, 2024
1 parent d64e699 commit 2b42de8
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 0 deletions.
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 cleanup mode.
*
* <p>The default cleanup mode may be changed via the
* {@value ParameterizedTestExtension#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
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 @@ -291,4 +292,21 @@
@API(status = STABLE, since = "5.10")
boolean autoCloseArguments() 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.
* This can also be controlled via the {@value ParameterizedTestExtension#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 @@ -15,9 +15,13 @@
import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.ExtensionConfigurationException;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
Expand All @@ -26,6 +30,8 @@
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.support.AnnotationConsumerInitializer;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.commons.util.Preconditions;

Expand All @@ -34,10 +40,13 @@
*/
class ParameterizedTestExtension implements TestTemplateInvocationContextProvider {

private static final Logger logger = LoggerFactory.getLogger(ParameterizedTestExtension.class);

private static final String METHOD_CONTEXT_KEY = "context";
static final String ARGUMENT_MAX_LENGTH_KEY = "junit.jupiter.params.displayname.argument.maxlength";
static final String DEFAULT_DISPLAY_NAME = "{default_display_name}";
static final String DISPLAY_NAME_PATTERN_KEY = "junit.jupiter.params.displayname.default";
static final String ARGUMENT_COUNT_VALIDATION_KEY = "junit.jupiter.params.argumentCountValidation";

@Override
public boolean supportsTestTemplate(ExtensionContext context) {
Expand Down Expand Up @@ -86,6 +95,7 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
.map(provider -> AnnotationConsumerInitializer.initialize(templateMethod, provider))
.flatMap(provider -> arguments(provider, extensionContext))
.map(arguments -> {
validateArgumentCount(extensionContext, arguments);
invocationCount.incrementAndGet();
return createInvocationContext(formatter, methodContext, arguments, invocationCount.intValue());
})
Expand All @@ -99,6 +109,55 @@ private ExtensionContext.Store getStore(ExtensionContext context) {
return context.getStore(Namespace.create(ParameterizedTestExtension.class, context.getRequiredTestMethod()));
}

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 = findAnnotation(//
extensionContext.getRequiredTestMethod(), ParameterizedTest.class//
).orElseThrow(NoSuchElementException::new);
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.DEFAULT;
Optional<String> optionalValue = extensionContext.getConfigurationParameter(key);
if (optionalValue.isPresent()) {
String value = optionalValue.get();
return Arrays.stream(ArgumentCountValidationMode.values()).filter(
mode -> mode.name().equalsIgnoreCase(value)).findFirst().orElseGet(() -> {
logger.warn(() -> String.format(
"Ignored invalid configuration '%s' set via the '%s' configuration parameter.", value, key));
return fallback;
});
}
else {
return fallback;
}
}

private TestTemplateInvocationContext createInvocationContext(ParameterizedTestNameFormatter formatter,
ParameterizedTestMethodContext methodContext, Arguments arguments, int invocationIndex) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,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 @@ -1093,6 +1094,42 @@ private EngineExecutionResults execute(String methodName, Class<?>... methodPara

}

@Nested
class UnusedArgumentsWithStrictArgumentsCountIntegrationTests {
@Test
void failsWithArgumentsSourceProvidingUnusedArguments() {
var results = execute(UnusedArgumentsTestCase.class, "testWithTwoUnusedStringArgumentsProvider",
String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1, event(EventConditions.finishedWithFailure(message(
"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(UnusedArgumentsTestCase.class, "testWithMethodSourceProvidingUnusedArguments",
String.class);
results.allEvents().assertThatEvents() //
.haveExactly(1, event(EventConditions.finishedWithFailure(message(
"Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided./nNote: the provided arguments are [foo, unused1]"))));
}

@Test
void executesWithMethodSourceProvidingUnusedArguments() {
var results = execute(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(Class<?> javaClass, String methodName,
Class<?>... methodParameterTypes) {
return EngineTestKit.engine(new JupiterTestEngine()).selectors(
selectMethod(javaClass, methodName, methodParameterTypes)).configurationParameter(
ParameterizedTestExtension.ARGUMENT_COUNT_VALIDATION_KEY, "strict").execute();
}
}

@Nested
class RepeatableSourcesIntegrationTests {

Expand Down

0 comments on commit 2b42de8

Please sign in to comment.