Skip to content

Commit

Permalink
Include parameter names in default name of @ParameterizedTest (#2108)
Browse files Browse the repository at this point in the history
Introduced a new `{arguments_with_names}` pattern in `@ParameterizedTest`, which is now used in the default name pattern. The parameter names are included only if they are present in the bytecode and if the parameter types are known (`ArgumentsAccessor` and `ArgumentsAggregator` are therefore ignored).

Closes #2040.
  • Loading branch information
juliette-derancourt authored Dec 1, 2019
1 parent ee3897b commit 26e7cab
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ on GitHub.
they are destroyed.
* `InvocationInterceptor` extensions may now explicitly `skip()` an intercepted
invocation. This allows executing it by other means, e.g. in a forked JVM.

* Parameter names are now included in the default display name of a `@ParameterizedTest`
invocation (if they are present in the bytecode). The `{argumentsWithNames}` pattern
can also be used in custom names.

[[release-notes-5.6.0-M2️-junit-vintage]]
=== JUnit Vintage
Expand Down
26 changes: 16 additions & 10 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -924,9 +924,9 @@ following.

....
palindromes(String) ✔
├─ [1] racecar ✔
├─ [2] radar ✔
└─ [3] able was I ere I saw elba ✔
├─ [1] candidate=racecar ✔
├─ [2] candidate=radar ✔
└─ [3] candidate=able was I ere I saw elba ✔
....

WARNING: Parameterized tests are currently an _experimental_ feature. Consult the table
Expand Down Expand Up @@ -1452,6 +1452,10 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=ArgumentsAggregator_w

By default, the display name of a parameterized test invocation contains the invocation
index and the `String` representation of all arguments for that specific invocation.
Each of them is preceded by the parameter name (unless the argument is only available via
an `ArgumentsAccessor` or `ArgumentAggregator`), if present in the bytecode (for Java,
test code must be compiled with the `-parameters` compiler flag).

However, you can customize invocation display names via the `name` attribute of the
`@ParameterizedTest` annotation like in the following example.

Expand All @@ -1465,20 +1469,22 @@ the following.

....
Display name of container ✔
├─ 1 ==> fruit='apple', rank=1 ✔
├─ 2 ==> fruit='banana', rank=2 ✔
└─ 3 ==> fruit='lemon, lime', rank=3 ✔
├─ 1 ==> the rank of 'apple' is 1 ✔
├─ 2 ==> the rank of 'banana' is 2 ✔
└─ 3 ==> the rank of 'lemon, lime' is 3 ✔
....

The following placeholders are supported within custom display names.

[cols="20,80"]
|===
| Placeholder | Description
| Placeholder | Description

| `{index}` | the current invocation index (1-based)
| `{arguments}` | the complete, comma-separated arguments list
| `{0}`, `{1}`, ... | an individual argument
| `{displayName}` | the display name of the method
| `{index}` | the current invocation index (1-based)
| `{arguments}` | the complete, comma-separated arguments list
| `{argumentsWithNames}` | the complete, comma-separated arguments list with parameter names
| `{0}`, `{1}`, ... | an individual argument
|===


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {

// tag::custom_display_names[]
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> fruit=''{0}'', rank={1}")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@
@API(status = EXPERIMENTAL, since = "5.3")
String ARGUMENTS_PLACEHOLDER = "{arguments}";

/**
* Placeholder for the complete, comma-separated named arguments list
* of the current invocation of a {@code @ParameterizedTest} method:
* <code>{argumentsWithNames}</code>
*
* @see #name
* @since 5.6
*/
@API(status = EXPERIMENTAL, since = "5.6")
String ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentsWithNames}";

/**
* Default display name pattern for the current invocation of a
* {@code @ParameterizedTest} method: {@value}
Expand All @@ -166,11 +177,11 @@
* @see #name
* @see #DISPLAY_NAME_PLACEHOLDER
* @see #INDEX_PLACEHOLDER
* @see #ARGUMENTS_PLACEHOLDER
* @see #ARGUMENTS_WITH_NAMES_PLACEHOLDER
* @since 5.3
*/
@API(status = EXPERIMENTAL, since = "5.3")
String DEFAULT_DISPLAY_NAME = "[" + INDEX_PLACEHOLDER + "] " + ARGUMENTS_PLACEHOLDER;
String DEFAULT_DISPLAY_NAME = "[" + INDEX_PLACEHOLDER + "] " + ARGUMENTS_WITH_NAMES_PLACEHOLDER;

/**
* The display name to be used for individual invocations of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
String displayName = extensionContext.getDisplayName();
ParameterizedTestMethodContext methodContext = getStore(extensionContext)//
.get(METHOD_CONTEXT_KEY, ParameterizedTestMethodContext.class);
ParameterizedTestNameFormatter formatter = createNameFormatter(templateMethod, displayName);
ParameterizedTestNameFormatter formatter = createNameFormatter(templateMethod, methodContext, displayName);
AtomicLong invocationCount = new AtomicLong(0);

// @formatter:off
Expand Down Expand Up @@ -118,13 +118,14 @@ private TestTemplateInvocationContext createInvocationContext(ParameterizedTestN
return new ParameterizedTestInvocationContext(formatter, methodContext, arguments);
}

private ParameterizedTestNameFormatter createNameFormatter(Method templateMethod, String displayName) {
private ParameterizedTestNameFormatter createNameFormatter(Method templateMethod,
ParameterizedTestMethodContext methodContext, String displayName) {
ParameterizedTest parameterizedTest = findAnnotation(templateMethod, ParameterizedTest.class).get();
String pattern = Preconditions.notBlank(parameterizedTest.name().trim(),
() -> String.format(
"Configuration error: @ParameterizedTest on method [%s] must be declared with a non-empty name.",
templateMethod));
return new ParameterizedTestNameFormatter(pattern, displayName);
return new ParameterizedTestNameFormatter(pattern, displayName, methodContext);
}

protected static Stream<? extends Arguments> arguments(ArgumentsProvider provider, ExtensionContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
Expand All @@ -42,14 +43,15 @@
*/
class ParameterizedTestMethodContext {

private final List<ResolverType> resolverTypes;
private final Parameter[] parameters;
private final Resolver[] resolvers;
private final List<ResolverType> resolverTypes;

ParameterizedTestMethodContext(Method testMethod) {
Parameter[] parameters = testMethod.getParameters();
this.resolverTypes = new ArrayList<>(parameters.length);
this.resolvers = new Resolver[parameters.length];
for (Parameter parameter : parameters) {
this.parameters = testMethod.getParameters();
this.resolvers = new Resolver[this.parameters.length];
this.resolverTypes = new ArrayList<>(this.parameters.length);
for (Parameter parameter : this.parameters) {
this.resolverTypes.add(isAggregator(parameter) ? AGGREGATOR : CONVERTER);
}
}
Expand Down Expand Up @@ -100,7 +102,27 @@ boolean hasPotentiallyValidSignature() {
* context.
*/
int getParameterCount() {
return resolvers.length;
return parameters.length;
}

/**
* Get the name of the {@link Parameter} with the supplied index, if
* it is present and declared before the aggregators.
*
* @return an {@code Optional} containing the name of the parameter
*/
Optional<String> getParameterName(int parameterIndex) {
if (parameterIndex >= getParameterCount()) {
return Optional.empty();
}
Parameter parameter = this.parameters[parameterIndex];
if (!parameter.isNamePresent()) {
return Optional.empty();
}
if (hasAggregator() && parameterIndex >= indexOfFirstAggregator()) {
return Optional.empty();
}
return Optional.of(parameter.getName());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_PLACEHOLDER;
import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_WITH_NAMES_PLACEHOLDER;
import static org.junit.jupiter.params.ParameterizedTest.DISPLAY_NAME_PLACEHOLDER;
import static org.junit.jupiter.params.ParameterizedTest.INDEX_PLACEHOLDER;

Expand All @@ -30,10 +31,12 @@ class ParameterizedTestNameFormatter {

private final String pattern;
private final String displayName;
private final ParameterizedTestMethodContext methodContext;

ParameterizedTestNameFormatter(String pattern, String displayName) {
ParameterizedTestNameFormatter(String pattern, String displayName, ParameterizedTestMethodContext methodContext) {
this.pattern = pattern;
this.displayName = displayName;
this.methodContext = methodContext;
}

String format(int invocationIndex, Object... arguments) {
Expand All @@ -59,18 +62,30 @@ private String prepareMessageFormatPattern(int invocationIndex, Object[] argumen
.replace(DISPLAY_NAME_PLACEHOLDER, this.displayName)//
.replace(INDEX_PLACEHOLDER, String.valueOf(invocationIndex));

if (result.contains(ARGUMENTS_WITH_NAMES_PLACEHOLDER)) {
result = result.replace(ARGUMENTS_WITH_NAMES_PLACEHOLDER, argumentsWithNamesPattern(arguments));
}

if (result.contains(ARGUMENTS_PLACEHOLDER)) {
// @formatter:off
String replacement = IntStream.range(0, arguments.length)
.mapToObj(index -> "{" + index + "}")
.collect(joining(", "));
// @formatter:on
result = result.replace(ARGUMENTS_PLACEHOLDER, replacement);
result = result.replace(ARGUMENTS_PLACEHOLDER, argumentsPattern(arguments));
}

return result;
}

private String argumentsWithNamesPattern(Object[] arguments) {
return IntStream.range(0, arguments.length) //
.mapToObj(index -> methodContext.getParameterName(index).map(name -> name + "=").orElse("") + "{"
+ index + "}") //
.collect(joining(", "));
}

private String argumentsPattern(Object[] arguments) {
return IntStream.range(0, arguments.length) //
.mapToObj(index -> "{" + index + "}") //
.collect(joining(", "));
}

private Object[] makeReadable(MessageFormat format, Object[] arguments) {
Format[] formats = format.getFormatsByArgumentIndex();
Object[] result = Arrays.copyOf(arguments, Math.min(arguments.length, formats.length), Object[].class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,26 @@ class ParameterizedTestParameterResolver implements ParameterResolver {
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
Executable declaringExecutable = parameterContext.getDeclaringExecutable();
Method testMethod = extensionContext.getTestMethod().orElse(null);
int parameterIndex = parameterContext.getIndex();

// Not a @ParameterizedTest method?
if (!declaringExecutable.equals(testMethod)) {
return false;
}

// Current parameter is an aggregator?
if (this.methodContext.isAggregator(parameterContext.getIndex())) {
if (this.methodContext.isAggregator(parameterIndex)) {
return true;
}

// Ensure that the current parameter is declared before aggregators.
// Otherwise, a different ParameterResolver should handle it.
if (this.methodContext.indexOfFirstAggregator() != -1) {
return parameterContext.getIndex() < this.methodContext.indexOfFirstAggregator();
if (this.methodContext.hasAggregator()) {
return parameterIndex < this.methodContext.indexOfFirstAggregator();
}

// Else fallback to behavior for parameterized test methods without aggregators.
return parameterContext.getIndex() < this.arguments.length;
return parameterIndex < this.arguments.length;
}

@Override
Expand Down
Loading

1 comment on commit 26e7cab

@sormuras
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Please sign in to comment.