diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M2.adoc index 54c67ada5c9c..3a3da0ba33eb 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M2.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M2.adoc @@ -45,8 +45,9 @@ JUnit repository on GitHub. [[release-notes-5.11.0-M2-junit-jupiter-new-features-and-improvements]] ==== New Features and Improvements -* ❓ - +* Support `@..Source` annotations as repeatable for parameterized tests. See the +<<../user-guide/index.adoc#writing-tests-parameterized-repeatable-sources, User Guide>> +for more details. [[release-notes-5.11.0-M2-junit-vintage]] === JUnit Vintage diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 756722623a2f..d3212f3a1c7b 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1963,6 +1963,34 @@ If you wish to implement a custom `ArgumentsProvider` that also consumes an anno (like built-in providers such as `{ValueArgumentsProvider}` or `{CsvArgumentsProvider}`), you have the possibility to extend the `{AnnotationBasedArgumentsProvider}` class. +[[writing-tests-parameterized-repeatable-sources]] +===== Multiple sources using repeatable annotations +Repeatable annotations provide a convenient way to specify multiple sources from +different providers. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedTestDemo.java[tags=repeatable_annotations] +---- + +Following the above parameterized test, a test case will run for each argument: + +---- +[1] foo +[2] bar +---- + +The following annotations are repeatable: + +* `@ValueSource` +* `@EnumSource` +* `@MethodSource` +* `@FieldSource` +* `@CsvSource` +* `@CsvFileSource` +* `@ArgumentsSource` + + [[writing-tests-parameterized-tests-argument-conversion]] ==== Argument Conversion diff --git a/documentation/src/test/java/example/ParameterizedTestDemo.java b/documentation/src/test/java/example/ParameterizedTestDemo.java index 0ca33aec886d..1b94e053519a 100644 --- a/documentation/src/test/java/example/ParameterizedTestDemo.java +++ b/documentation/src/test/java/example/ParameterizedTestDemo.java @@ -542,4 +542,22 @@ static Stream namedArguments() { } // end::named_arguments[] // @formatter:on + + // tag::repeatable_annotations[] + @DisplayName("A parameterized test that makes use of repeatable annotations") + @ParameterizedTest + @MethodSource("someProvider") + @MethodSource("otherProvider") + void testWithRepeatedAnnotation(String argument) { + assertNotNull(argument); + } + + static Stream someProvider() { + return Stream.of("foo"); + } + + static Stream otherProvider() { + return Stream.of("bar"); + } + // end::repeatable_annotations[] } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java index 0b950ef71d8f..f751b35e3c5c 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java @@ -13,6 +13,8 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import org.apiguardian.api.API; @@ -39,17 +41,17 @@ public abstract class AnnotationBasedArgumentsProvider public AnnotationBasedArgumentsProvider() { } - private A annotation; + private final List annotations = new ArrayList<>(); @Override public final void accept(A annotation) { Preconditions.notNull(annotation, "annotation must not be null"); - this.annotation = annotation; + annotations.add(annotation); } @Override public final Stream provideArguments(ExtensionContext context) { - return provideArguments(context, this.annotation); + return annotations.stream().flatMap(annotation -> provideArguments(context, annotation)); } /** diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java index 6017132348d5..1798dfc171b3 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -21,9 +22,9 @@ import org.apiguardian.api.API; /** - * {@code @CsvFileSource} is an {@link ArgumentsSource} which is used to load - * comma-separated value (CSV) files from one or more classpath {@link #resources} - * or {@link #files}. + * {@code @CsvFileSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which is used to load comma-separated value (CSV) + * files from one or more classpath {@link #resources} or {@link #files}. * *

The CSV records parsed from these resources and files will be provided as * arguments to the annotated {@code @ParameterizedTest} method. Note that the @@ -63,6 +64,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(CsvFileSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(CsvFileArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java new file mode 100644 index 000000000000..bc6bf3503fc9 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java @@ -0,0 +1,46 @@ +/* + * 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.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @CsvFileSources} is a simple container for one or more + * {@link CsvFileSource} annotations. + * + *

Note, however, that use of the {@code @CsvFileSources} container is completely + * optional since {@code @CsvFileSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see CsvFileSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface CsvFileSources { + + /** + * An array of one or more {@link CsvFileSource @CsvFileSource} + * annotations. + */ + CsvFileSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java index ecf3ca0848ee..ef09eea27ba6 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -21,9 +22,10 @@ import org.apiguardian.api.API; /** - * {@code @CsvSource} is an {@link ArgumentsSource} which reads comma-separated - * values (CSV) from one or more CSV records supplied via the {@link #value} - * attribute or {@link #textBlock} attribute. + * {@code @CsvSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which reads comma-separated values (CSV) from one + * or more CSV records supplied via the {@link #value} attribute or + * {@link #textBlock} attribute. * *

The supplied values will be provided as arguments to the annotated * {@code @ParameterizedTest} method. @@ -64,6 +66,7 @@ */ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) +@Repeatable(CsvSources.class) @Documented @API(status = STABLE, since = "5.7") @ArgumentsSource(CsvArgumentsProvider.class) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java new file mode 100644 index 000000000000..6c6951a75beb --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java @@ -0,0 +1,46 @@ +/* + * 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.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @CsvSources} is a simple container for one or more + * {@link CsvSource} annotations. + * + *

Note, however, that use of the {@code @CsvSources} container is completely + * optional since {@code @CsvSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see CsvSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface CsvSources { + + /** + * An array of one or more {@link CsvSource @CsvSource} + * annotations. + */ + CsvSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java index ab05a56cf8ac..3bf7e9b88e5e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java @@ -16,6 +16,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -29,8 +30,8 @@ import org.junit.platform.commons.util.Preconditions; /** - * {@code @EnumSource} is an {@link ArgumentsSource} for constants of - * an {@link Enum}. + * {@code @EnumSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} for constants of an {@link Enum}. * *

The enum constants will be provided as arguments to the annotated * {@code @ParameterizedTest} method. @@ -49,6 +50,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(EnumSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(EnumArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java new file mode 100644 index 000000000000..22feb5aa46d6 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java @@ -0,0 +1,46 @@ +/* + * 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.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @EnumSources} is a simple container for one or more + * {@link EnumSource} annotations. + * + *

Note, however, that use of the {@code @EnumSources} container is completely + * optional since {@code @EnumSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see EnumSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface EnumSources { + + /** + * An array of one or more {@link EnumSource @EnumSource} + * annotations. + */ + EnumSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java index 1d3198ceb529..77680a00b7d1 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -22,10 +23,11 @@ import org.junit.jupiter.params.ParameterizedTest; /** - * {@code @FieldSource} is an {@link ArgumentsSource} which provides access to - * values of {@linkplain #value() fields} of the class in which this annotation - * is declared or from static fields in external classes referenced by - * fully qualified field name. + * {@code @FieldSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which provides access to values of + * {@linkplain #value() fields} of the class in which this annotation is declared + * or from static fields in external classes referenced by fully qualified + * field name. * *

Each field must be able to supply a stream of arguments, * and each set of "arguments" within the "stream" will be provided as the physical @@ -112,6 +114,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(FieldSources.class) @API(status = EXPERIMENTAL, since = "5.11") @ArgumentsSource(FieldArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java new file mode 100644 index 000000000000..0b46746db5e4 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java @@ -0,0 +1,46 @@ +/* + * 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.provider; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @FieldSources} is a simple container for one or more + * {@link FieldSource} annotations. + * + *

Note, however, that use of the {@code @FieldSources} container is completely + * optional since {@code @FieldSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see FieldSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = EXPERIMENTAL, since = "5.11") +public @interface FieldSources { + + /** + * An array of one or more {@link FieldSource @FieldSource} + * annotations. + */ + FieldSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java index 72404ee1063e..977e7555a5d2 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -22,10 +23,11 @@ import org.junit.jupiter.params.ParameterizedTest; /** - * {@code @MethodSource} is an {@link ArgumentsSource} which provides access - * to values returned from {@linkplain #value() factory methods} of the class in - * which this annotation is declared or from static factory methods in external - * classes referenced by fully qualified method name. + * {@code @MethodSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which provides access to values returned from + * {@linkplain #value() factory methods} of the class in which this annotation + * is declared or from static factory methods in external classes referenced + * by fully qualified method name. * *

Each factory method must generate a stream of arguments, * and each set of "arguments" within the "stream" will be provided as the physical @@ -103,6 +105,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(MethodSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(MethodArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java new file mode 100644 index 000000000000..056453f29820 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java @@ -0,0 +1,46 @@ +/* + * 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.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @MethodSources} is a simple container for one or more + * {@link MethodSource} annotations. + * + *

Note, however, that use of the {@code @MethodSources} container is completely + * optional since {@code @MethodSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see MethodSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface MethodSources { + + /** + * An array of one or more {@link MethodSource @MethodSource} + * annotations. + */ + MethodSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java index d2ae43eb03e4..bc0ed303e935 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -21,8 +22,8 @@ import org.apiguardian.api.API; /** - * {@code @ValueSource} is an {@link ArgumentsSource} which provides access to - * an array of literal values. + * {@code @ValueSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which provides access to an array of literal values. * *

Supported types include {@link #shorts}, {@link #bytes}, {@link #ints}, * {@link #longs}, {@link #floats}, {@link #doubles}, {@link #chars}, @@ -40,6 +41,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(ValueSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(ValueArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java new file mode 100644 index 000000000000..8db4dcc5b01f --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java @@ -0,0 +1,46 @@ +/* + * 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.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @ValueSources} is a simple container for one or more + * {@link ValueSource} annotations. + * + *

Note, however, that use of the {@code @ValueSources} container is completely + * optional since {@code @ValueSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see ValueSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface ValueSources { + + /** + * An array of one or more {@link ValueSource @ValueSource} + * annotations. + */ + ValueSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java index 94814e7c5bf2..9296c70deada 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java @@ -11,19 +11,23 @@ package org.junit.jupiter.params.support; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; +import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP; import static org.junit.platform.commons.util.ReflectionUtils.findMethods; import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.Collections; import java.util.List; import java.util.function.Predicate; import org.apiguardian.api.API; import org.junit.platform.commons.JUnitException; -import org.junit.platform.commons.util.AnnotationUtils; /** * {@code AnnotationConsumerInitializer} is an internal helper class for @@ -47,14 +51,27 @@ private AnnotationConsumerInitializer() { public static T initialize(AnnotatedElement annotatedElement, T annotationConsumerInstance) { if (annotationConsumerInstance instanceof AnnotationConsumer) { Class annotationType = findConsumedAnnotationType(annotationConsumerInstance); - Annotation annotation = AnnotationUtils.findAnnotation(annotatedElement, annotationType) // - .orElseThrow(() -> new JUnitException(annotationConsumerInstance.getClass().getName() - + " must be used with an annotation of type " + annotationType.getName())); - initializeAnnotationConsumer((AnnotationConsumer) annotationConsumerInstance, annotation); + List annotations = findAnnotations(annotatedElement, annotationType); + + if (annotations.isEmpty()) { + throw new JUnitException(annotationConsumerInstance.getClass().getName() + + " must be used with an annotation of type " + annotationType.getName()); + } + + annotations.forEach(annotation -> initializeAnnotationConsumer( + (AnnotationConsumer) annotationConsumerInstance, annotation)); } return annotationConsumerInstance; } + private static List findAnnotations(AnnotatedElement annotatedElement, + Class annotationType) { + + return annotationType.isAnnotationPresent(Repeatable.class) + ? findRepeatableAnnotations(annotatedElement, annotationType) + : findAnnotation(annotatedElement, annotationType).map(Collections::singletonList).orElse(emptyList()); + } + private static Class findConsumedAnnotationType(T annotationConsumerInstance) { Predicate consumesAnnotation = annotationConsumingMethodSignatures.stream() // .map(signature -> (Predicate) signature::isMatchingWith) // diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 69a28343706a..f1222b706bc0 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -84,6 +84,7 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.jupiter.params.ParameterizedTestIntegrationTests.RepeatableSourcesTestCase.Action; import org.junit.jupiter.params.aggregator.AggregateWith; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; @@ -97,6 +98,7 @@ import org.junit.jupiter.params.provider.CsvFileSource; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.FieldSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; @@ -1070,6 +1072,101 @@ private EngineExecutionResults execute(String methodName, Class... methodPara } + @Nested + class RepeatableSourcesIntegrationTests { + + @Test + void executesWithRepeatableCsvFileSource() { + var results = execute("testWithRepeatableCsvFileSource", String.class, String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, + event(test(), displayName("[1] column1=foo, column2=1"), finishedWithFailure(message("foo 1")))) // + .haveExactly(1, event(test(), displayName("[5] column1=FRUIT = apple, column2=RANK = 1"), + finishedWithFailure(message("apple 1")))); + } + + @Test + void executesWithRepeatableCsvSource() { + var results = execute("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")))); + } + + @Test + void executesWithRepeatableMethodSource() { + var results = execute("testWithRepeatableMethodSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, + event(test(), displayName("[1] argument=some"), finishedWithFailure(message("some")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=other"), finishedWithFailure(message("other")))); + } + + @Test + void executesWithRepeatableEnumSource() { + var results = execute("testWithRepeatableEnumSource", Action.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=FOO"), finishedWithFailure(message("FOO")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=BAR"), finishedWithFailure(message("BAR")))); + } + + @Test + void executesWithRepeatableValueSource() { + var results = execute("testWithRepeatableValueSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))); + } + + @Test + void executesWithRepeatableFieldSource() { + var results = execute("testWithRepeatableFieldSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, + event(test(), displayName("[1] argument=some"), finishedWithFailure(message("some")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=other"), finishedWithFailure(message("other")))); + } + + @Test + void executesWithRepeatableArgumentsSource() { + var results = execute("testWithRepeatableArgumentsSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) // + .haveExactly(1, event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))) // + .haveExactly(1, event(test(), displayName("[3] argument=foo"), finishedWithFailure(message("foo")))) // + .haveExactly(1, + event(test(), displayName("[4] argument=bar"), finishedWithFailure(message("bar")))); + + } + + @Test + void executesWithSameRepeatableAnnotationMultipleTimes() { + var results = execute("testWithSameRepeatableAnnotationMultipleTimes", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), started())) // + .haveExactly(1, event(test(), finishedWithFailure(message("foo")))); + } + + @Test + void executesWithDifferentRepeatableAnnotations() { + var results = execute("testWithDifferentRepeatableAnnotations", 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")))) // + .haveExactly(1, event(test(), displayName("[3] argument=c"), finishedWithFailure(message("c")))) // + .haveExactly(1, event(test(), displayName("[4] argument=d"), finishedWithFailure(message("d")))); + } + + private EngineExecutionResults execute(String methodName, Class... methodParameterTypes) { + return ParameterizedTestIntegrationTests.this.execute(RepeatableSourcesTestCase.class, methodName, + methodParameterTypes); + } + } + @Test void closeAutoCloseableArgumentsAfterTest() { var results = execute("testWithAutoCloseableArgument", AutoCloseableArgument.class); @@ -1905,6 +2002,99 @@ static Stream providerMethod() { } + static class RepeatableSourcesTestCase { + + @ParameterizedTest + @CsvFileSource(resources = "two-column.csv") + @CsvFileSource(resources = "two-column-with-headers.csv", delimiter = '|', useHeadersInDisplayName = true, nullValues = "NIL") + void testWithRepeatableCsvFileSource(String column1, String column2) { + fail("%s %s".formatted(column1, column2)); + } + + @ParameterizedTest + @CsvSource({ "a" }) + @CsvSource({ "b" }) + void testWithRepeatableCsvSource(String argument) { + fail(argument); + } + + @ParameterizedTest + @EnumSource(SmartAction.class) + @EnumSource(QuickAction.class) + void testWithRepeatableEnumSource(Action argument) { + fail(argument.toString()); + } + + interface Action { + } + + private enum SmartAction implements Action { + FOO + } + + private enum QuickAction implements Action { + BAR + } + + @ParameterizedTest + @MethodSource("someArgumentsMethodSource") + @MethodSource("otherArgumentsMethodSource") + void testWithRepeatableMethodSource(String argument) { + fail(argument); + } + + public static Stream someArgumentsMethodSource() { + return Stream.of(Arguments.of("some")); + } + + public static Stream otherArgumentsMethodSource() { + return Stream.of(Arguments.of("other")); + } + + @ParameterizedTest + @FieldSource("someArgumentsContainer") + @FieldSource("otherArgumentsContainer") + void testWithRepeatableFieldSource(String argument) { + fail(argument); + } + + static List someArgumentsContainer = List.of("some"); + static List otherArgumentsContainer = List.of("other"); + + @ParameterizedTest + @ValueSource(strings = "foo") + @ValueSource(strings = "bar") + void testWithRepeatableValueSource(String argument) { + fail(argument); + } + + @ParameterizedTest + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + void testWithSameRepeatableAnnotationMultipleTimes(String argument) { + fail(argument); + } + + @ParameterizedTest + @ValueSource(strings = "a") + @ValueSource(strings = "b") + @CsvSource({ "c" }) + @CsvSource({ "d" }) + void testWithDifferentRepeatableAnnotations(String argument) { + fail(argument); + } + + @ParameterizedTest + @ArgumentsSource(TwoSingleStringArgumentsProvider.class) + @ArgumentsSource(TwoUnusedStringArgumentsProvider.class) + void testWithRepeatableArgumentsSource(String argument) { + fail(argument); + } + } + private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider { @Override diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java index 25ffd4e1d484..af6e1eec06bd 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java @@ -10,6 +10,7 @@ package org.junit.jupiter.params.provider; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.params.provider.MockCsvAnnotationBuilder.csvSource; import static org.mockito.ArgumentMatchers.eq; @@ -30,7 +31,7 @@ class AnnotationBasedArgumentsProviderTests { private final AnnotationBasedArgumentsProvider annotationBasedArgumentsProvider = new AnnotationBasedArgumentsProvider<>() { @Override protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { - return Stream.empty(); + return Stream.of(Arguments.of(annotation)); } }; @@ -54,4 +55,21 @@ void shouldInvokeTemplateMethodWithTheAnnotationProvidedToAccept() { verify(spiedProvider, atMostOnce()).provideArguments(eq(extensionContext), eq(annotation)); } + @Test + @DisplayName("should invoke the provideArguments template method for every accepted annotation") + void shouldInvokeTemplateMethodForEachAnnotationProvided() { + var extensionContext = mock(ExtensionContext.class); + var foo = csvSource("foo"); + var bar = csvSource("bar"); + + annotationBasedArgumentsProvider.accept(foo); + annotationBasedArgumentsProvider.accept(bar); + + var arguments = annotationBasedArgumentsProvider.provideArguments(extensionContext).toList(); + + assertThat(arguments).hasSize(2); + assertThat(arguments.getFirst().get()[0]).isEqualTo(foo); + assertThat(arguments.get(1).get()[0]).isEqualTo(bar); + } + } diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java index a7ecddec1f7f..0ea40f9c70e9 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java @@ -40,7 +40,7 @@ void throwsExceptionIfNeitherValueNorTextBlockIsDeclared() { var annotation = csvSource().build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessage("@CsvSource must be declared with either `value` or `textBlock` but not both"); } @@ -52,7 +52,7 @@ void throwsExceptionIfValueAndTextBlockAreDeclared() { """).build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessage("@CsvSource must be declared with either `value` or `textBlock` but not both"); } @@ -223,7 +223,7 @@ void throwsExceptionIfBothDelimitersAreSimultaneouslySet() { var annotation = csvSource().delimiter('|').delimiterString("~~~").build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("The delimiter and delimiterString attributes cannot be set simultaneously in")// .withMessageContaining("CsvSource"); } @@ -270,7 +270,7 @@ void throwsExceptionIfSourceExceedsMaxCharsPerColumnConfig() { var annotation = csvSource().lines("413").maxCharsPerColumn(2).build(); assertThatExceptionOfType(CsvParsingException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("Failed to parse CSV input configured via Mock for CsvSource")// .withRootCauseInstanceOf(ArrayIndexOutOfBoundsException.class); } @@ -289,7 +289,7 @@ void throwsExceptionWhenSourceExceedsDefaultMaxCharsPerColumnConfig() { var annotation = csvSource().lines("0".repeat(4097)).delimiter(';').build(); assertThatExceptionOfType(CsvParsingException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("Failed to parse CSV input configured via Mock for CsvSource")// .withRootCauseInstanceOf(ArrayIndexOutOfBoundsException.class); } @@ -308,7 +308,7 @@ void throwsExceptionWhenMaxCharsPerColumnIsNotPositiveNumber() { var annotation = csvSource().lines("41").delimiter(';').maxCharsPerColumn(-1).build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("maxCharsPerColumn must be a positive number: -1"); } @@ -372,7 +372,7 @@ void throwsExceptionIfColumnCountExceedsHeaderCount() { """).build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessage( "The number of columns (3) exceeds the number of supplied headers (2) in CSV record: [banana, 2, BOOM!]"); } diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java index e7c061a2ecd4..afdce4749143 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java @@ -98,7 +98,8 @@ void throwsExceptionIfBothDelimitersAreSimultaneouslySet() { .delimiterString(";")// .build(); - var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(annotation, "foo")); + var exception = assertThrows(PreconditionViolationException.class, + () -> provideArguments(annotation, "foo").findAny()); assertThat(exception)// .hasMessageStartingWith("The delimiter and delimiterString attributes cannot be set simultaneously in")// @@ -435,7 +436,7 @@ void throwsExceptionWhenMaxCharsPerColumnIsNotPositiveNumber(@TempDir Path tempD .build(); var exception = assertThrows(PreconditionViolationException.class, // - () -> provideArguments(new CsvFileArgumentsProvider(), annotation)); + () -> provideArguments(new CsvFileArgumentsProvider(), annotation).findAny()); assertThat(exception)// .hasMessageStartingWith("maxCharsPerColumn must be a positive number: -1"); diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java index f63514b1be3a..6a5312085776 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java @@ -56,21 +56,21 @@ void provideAllEnumConstantsWithNamingAll() { @Test void duplicateConstantNameIsDetected() { Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(EnumWithTwoConstants.class, "FOO", "BAR", "FOO")); + () -> provideArguments(EnumWithTwoConstants.class, "FOO", "BAR", "FOO").findAny()); assertThat(exception).hasMessageContaining("Duplicate enum constant name(s) found"); } @Test void invalidConstantNameIsDetected() { Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(EnumWithTwoConstants.class, "FO0", "B4R")); + () -> provideArguments(EnumWithTwoConstants.class, "FO0", "B4R").findAny()); assertThat(exception).hasMessageContaining("Invalid enum constant name(s) in"); } @Test void invalidPatternIsDetected() { Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(EnumWithTwoConstants.class, Mode.MATCH_ALL, "(", ")")); + () -> provideArguments(EnumWithTwoConstants.class, Mode.MATCH_ALL, "(", ")").findAny()); assertThat(exception).hasMessageContaining("Pattern compilation failed"); } @@ -90,7 +90,7 @@ void incorrectParameterTypeIsDetected() throws Exception { TestCase.class.getDeclaredMethod("methodWithIncorrectParameter", Object.class)); Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(NullEnum.class)); + () -> provideArguments(NullEnum.class).findAny()); assertThat(exception).hasMessageStartingWith("First parameter must reference an Enum type"); } @@ -100,7 +100,7 @@ void methodsWithoutParametersAreDetected() throws Exception { TestCase.class.getDeclaredMethod("methodWithoutParameters")); Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(NullEnum.class)); + () -> provideArguments(NullEnum.class).findAny()); assertThat(exception).hasMessageStartingWith("Test method must declare at least one parameter"); } diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java index 86b2221aec94..dea71ff0f727 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java @@ -29,7 +29,7 @@ class ValueArgumentsProviderTests { void multipleInputsAreNotAllowed() { var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(new short[1], new byte[0], new int[1], new long[0], new float[0], new double[0], - new char[0], new boolean[0], new String[0], new Class[0])); + new char[0], new boolean[0], new String[0], new Class[0]).findAny()); assertThat(exception).hasMessageContaining( "Exactly one type of input must be provided in the @ValueSource annotation, but there were 2"); @@ -39,7 +39,7 @@ void multipleInputsAreNotAllowed() { void onlyEmptyInputsAreNotAllowed() { var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(new short[0], new byte[0], new int[0], new long[0], new float[0], new double[0], - new char[0], new boolean[0], new String[0], new Class[0])); + new char[0], new boolean[0], new String[0], new Class[0]).findAny()); assertThat(exception).hasMessageContaining( "Exactly one type of input must be provided in the @ValueSource annotation, but there were 0"); diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java index 7e3c34b3ab91..b60da429e03e 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java @@ -13,10 +13,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.params.support.AnnotationConsumerInitializer.initialize; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; @@ -52,9 +58,11 @@ void shouldInitializeAnnotationBasedArgumentsProvider() throws NoSuchMethodExcep var method = SubjectClass.class.getDeclaredMethod("foo"); var initialisedAnnotationConsumer = initialize(method, instance); - initialisedAnnotationConsumer.provideArguments(mock()); + initialisedAnnotationConsumer.provideArguments(mock()).findAny(); - assertThat(initialisedAnnotationConsumer.annotation) // + assertThat(initialisedAnnotationConsumer.annotations) // + .hasSize(1) // + .element(0) // .isInstanceOfSatisfying(CsvSource.class, // source -> assertThat(source.value()).containsExactly("a", "b")); } @@ -93,13 +101,23 @@ void shouldThrowExceptionWhenParameterIsNotAnnotated() throws NoSuchMethodExcept assertThatThrownBy(() -> initialize(parameter, instance)).isInstanceOf(JUnitException.class); } + @Test + void shouldInitializeForEachAnnotations() throws NoSuchMethodException { + var instance = spy(new SomeAnnotationBasedArgumentsProvider()); + var method = SubjectClass.class.getDeclaredMethod("repeatableAnnotation", String.class); + + initialize(method, instance); + + verify(instance, times(2)).accept(any(CsvSource.class)); + } + private static class SomeAnnotationBasedArgumentsProvider extends AnnotationBasedArgumentsProvider { - CsvSource annotation; + List annotations = new ArrayList<>(); @Override protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { - this.annotation = annotation; + annotations.add(annotation); return Stream.empty(); } } @@ -138,6 +156,11 @@ void bar(@JavaTimeConversionPattern("pattern") LocalDate date) { void noAnnotation(String param) { } + + @CsvSource("a") + @CsvSource("b") + void repeatableAnnotation(String param) { + } } }