diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 3c29620bee9a..1067b32a0ba1 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -137,6 +137,8 @@ JUnit repository on GitHub. thread dump to `System.out` prior to interrupting a test thread due to a timeout. * `TestReporter` now allows publishing files for a test method or test class which can be used to include them in test reports, such as the Open Test Reporting format. +* New `from` and `to` attributes added to `@EnumSource` to support range selection of + enum constants. [[release-notes-5.12.0-M1-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 25a442d6c295..286727143a94 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1560,14 +1560,28 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_example_au ---- The annotation provides an optional `names` attribute that lets you specify which -constants shall be used, like in the following example. If omitted, all constants will be -used. +constants shall be used, like in the following example. [source,java,indent=0] ---- include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_include_example] ---- +In addition to `names`, you can use the `from` and `to` attributes to specify a range of +constants. The range starts from the constant specified in the `from` attribute and +includes all subsequent constants up to and including the one specified in the `to` +attribute, based on the natural order of the enum constants. + +If `from` and `to` attributes are omitted, they default to the first and last constants +in the enum type, respectively. If all `names`, `from`, and `to` attributes are omitted, +all constants will be used. The following example demonstrates how to specify a range of +constants. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_range_example] +---- + The `@EnumSource` annotation also provides an optional `mode` attribute that enables fine-grained control over which constants are passed to the test method. For example, you can exclude names from the enum constant pool or specify regular expressions as in the @@ -1583,6 +1597,14 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_exclude_ex include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_regex_example] ---- +You can also combine `mode` with the `from`, `to` and `names` attributes to define a +range of constants while excluding specific values from that range as shown below. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_range_exclude_example] +---- + [[writing-tests-parameterized-tests-sources-MethodSource]] ===== @MethodSource diff --git a/documentation/src/test/java/example/ParameterizedTestDemo.java b/documentation/src/test/java/example/ParameterizedTestDemo.java index 894b7617761d..4a200726ebcd 100644 --- a/documentation/src/test/java/example/ParameterizedTestDemo.java +++ b/documentation/src/test/java/example/ParameterizedTestDemo.java @@ -148,6 +148,14 @@ void testWithEnumSourceInclude(ChronoUnit unit) { } // end::EnumSource_include_example[] + // tag::EnumSource_range_example[] + @ParameterizedTest + @EnumSource(from = "HOURS", to = "DAYS") + void testWithEnumSourceRange(ChronoUnit unit) { + assertTrue(EnumSet.of(ChronoUnit.HOURS, ChronoUnit.HALF_DAYS, ChronoUnit.DAYS).contains(unit)); + } + // end::EnumSource_range_example[] + // tag::EnumSource_exclude_example[] @ParameterizedTest @EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" }) @@ -164,6 +172,15 @@ void testWithEnumSourceRegex(ChronoUnit unit) { } // end::EnumSource_regex_example[] + // tag::EnumSource_range_exclude_example[] + @ParameterizedTest + @EnumSource(mode = EXCLUDE, from = "HOURS", to = "DAYS", names = { "HALF_DAYS" }) + void testWithEnumSourceRangeExclude(ChronoUnit unit) { + assertTrue(EnumSet.of(ChronoUnit.HOURS, ChronoUnit.DAYS).contains(unit)); + assertFalse(EnumSet.of(ChronoUnit.HALF_DAYS).contains(unit)); + } + // end::EnumSource_range_exclude_example[] + // tag::simple_MethodSource_example[] @ParameterizedTest @MethodSource("stringProvider") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java index 0a525289e566..a68b43d5f371 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java @@ -43,7 +43,13 @@ protected Stream provideArguments(ExtensionContext context, private > Set getEnumConstants(ExtensionContext context, EnumSource enumSource) { Class enumClass = determineEnumClass(context, enumSource); - return EnumSet.allOf(enumClass); + E[] constants = enumClass.getEnumConstants(); + if (constants.length == 0) { + return EnumSet.noneOf(enumClass); + } + E from = enumSource.from().isEmpty() ? constants[0] : Enum.valueOf(enumClass, enumSource.from()); + E to = enumSource.to().isEmpty() ? constants[constants.length - 1] : Enum.valueOf(enumClass, enumSource.to()); + return EnumSet.range(from, to); } @SuppressWarnings({ "unchecked", "rawtypes" }) 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 3bf7e9b88e5e..673066b163b2 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 @@ -40,8 +40,8 @@ * attribute. Otherwise, the declared type of the first parameter of the * {@code @ParameterizedTest} method is used. * - *

The set of enum constants can be restricted via the {@link #names} and - * {@link #mode} attributes. + *

The set of enum constants can be restricted via the {@link #names}, + * {@link #from}, {@link #to} and {@link #mode} attributes. * * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsSource @@ -63,6 +63,8 @@ * first parameter of the {@code @ParameterizedTest} method is used. * * @see #names + * @see #from + * @see #to * @see #mode */ Class> value() default NullEnum.class; @@ -71,16 +73,48 @@ * The names of enum constants to provide, or regular expressions to select * the names of enum constants to provide. * - *

If no names or regular expressions are specified, all enum constants - * declared in the specified {@linkplain #value enum type} will be provided. + *

If no names or regular expressions are specified, and neither {@link #from} + * nor {@link #to} are specified, all enum constants declared in the specified + * {@linkplain #value enum type} will be provided. + * + *

If {@link #from} or {@link #to} are specified, the elements in names must + * fall within the range defined by {@link #from} and {@link #to}. * *

The {@link #mode} determines how the names are interpreted. * * @see #value + * @see #from + * @see #to * @see #mode */ String[] names() default {}; + /** + * The starting enum constant of the range to be included. + * + *

Defaults to an empty string, where the range starts from the first enum + * constant of the specified {@linkplain #value enum type}. + * + * @see #value + * @see #names + * @see #to + * @see #mode + */ + String from() default ""; + + /** + * The ending enum constant of the range to be included. + * + *

Defaults to an empty string, where the range ends at the last enum + * constant of the specified {@linkplain #value enum type}. + * + * @see #value + * @see #names + * @see #from + * @see #mode + */ + String to() default ""; + /** * The enum constant selection mode. * diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java index 6a5312085776..a3c405dcf23b 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java @@ -12,8 +12,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.params.provider.EnumArgumentsProviderTests.EnumWithTwoConstants.BAR; -import static org.junit.jupiter.params.provider.EnumArgumentsProviderTests.EnumWithTwoConstants.FOO; +import static org.junit.jupiter.params.provider.EnumArgumentsProviderTests.EnumWithFourConstants.BAR; +import static org.junit.jupiter.params.provider.EnumArgumentsProviderTests.EnumWithFourConstants.BAZ; +import static org.junit.jupiter.params.provider.EnumArgumentsProviderTests.EnumWithFourConstants.FOO; +import static org.junit.jupiter.params.provider.EnumArgumentsProviderTests.EnumWithFourConstants.QUX; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -34,21 +36,22 @@ class EnumArgumentsProviderTests { @Test void providesAllEnumConstants() { - var arguments = provideArguments(EnumWithTwoConstants.class); + var arguments = provideArguments(EnumWithFourConstants.class); - assertThat(arguments).containsExactly(new Object[] { FOO }, new Object[] { BAR }); + assertThat(arguments).containsExactly(new Object[] { FOO }, new Object[] { BAR }, new Object[] { BAZ }, + new Object[] { QUX }); } @Test void provideSingleEnumConstant() { - var arguments = provideArguments(EnumWithTwoConstants.class, "FOO"); + var arguments = provideArguments(EnumWithFourConstants.class, "FOO"); assertThat(arguments).containsExactly(new Object[] { FOO }); } @Test void provideAllEnumConstantsWithNamingAll() { - var arguments = provideArguments(EnumWithTwoConstants.class, "FOO", "BAR"); + var arguments = provideArguments(EnumWithFourConstants.class, "FOO", "BAR"); assertThat(arguments).containsExactly(new Object[] { FOO }, new Object[] { BAR }); } @@ -56,32 +59,33 @@ void provideAllEnumConstantsWithNamingAll() { @Test void duplicateConstantNameIsDetected() { Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(EnumWithTwoConstants.class, "FOO", "BAR", "FOO").findAny()); + () -> provideArguments(EnumWithFourConstants.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").findAny()); + () -> provideArguments(EnumWithFourConstants.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, "(", ")").findAny()); + () -> provideArguments(EnumWithFourConstants.class, Mode.MATCH_ALL, "(", ")").findAny()); assertThat(exception).hasMessageContaining("Pattern compilation failed"); } @Test void providesEnumConstantsBasedOnTestMethod() throws Exception { when(extensionContext.getRequiredTestMethod()).thenReturn( - TestCase.class.getDeclaredMethod("methodWithCorrectParameter", EnumWithTwoConstants.class)); + TestCase.class.getDeclaredMethod("methodWithCorrectParameter", EnumWithFourConstants.class)); var arguments = provideArguments(NullEnum.class); - assertThat(arguments).containsExactly(new Object[] { FOO }, new Object[] { BAR }); + assertThat(arguments).containsExactly(new Object[] { FOO }, new Object[] { BAR }, new Object[] { BAZ }, + new Object[] { QUX }); } @Test @@ -104,8 +108,64 @@ void methodsWithoutParametersAreDetected() throws Exception { assertThat(exception).hasMessageStartingWith("Test method must declare at least one parameter"); } + @Test + void providesEnumConstantsStartingFromBar() { + var arguments = provideArguments(EnumWithFourConstants.class, "BAR", "", Mode.INCLUDE); + + assertThat(arguments).containsExactly(new Object[] { BAR }, new Object[] { BAZ }, new Object[] { QUX }); + } + + @Test + void providesEnumConstantsEndingAtBaz() { + var arguments = provideArguments(EnumWithFourConstants.class, "", "BAZ", Mode.INCLUDE); + + assertThat(arguments).containsExactly(new Object[] { FOO }, new Object[] { BAR }, new Object[] { BAZ }); + } + + @Test + void providesEnumConstantsFromBarToBaz() { + var arguments = provideArguments(EnumWithFourConstants.class, "BAR", "BAZ", Mode.INCLUDE); + + assertThat(arguments).containsExactly(new Object[] { BAR }, new Object[] { BAZ }); + } + + @Test + void providesEnumConstantsFromFooToBazWhileExcludingBar() { + var arguments = provideArguments(EnumWithFourConstants.class, "FOO", "BAZ", Mode.EXCLUDE, "BAR"); + + assertThat(arguments).containsExactly(new Object[] { FOO }, new Object[] { BAZ }); + } + + @Test + void providesNoEnumConstant() { + var arguments = provideArguments(EnumWithNoConstant.class); + + assertThat(arguments).isEmpty(); + } + + @Test + void invalidConstantNameIsDetectedInRange() { + Exception exception = assertThrows(PreconditionViolationException.class, + () -> provideArguments(EnumWithFourConstants.class, "FOO", "BAZ", Mode.EXCLUDE, "QUX").findAny()); + assertThat(exception).hasMessageContaining("Invalid enum constant name(s) in"); + } + + @Test + void invalidStartingRangeIsDetected() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> provideArguments(EnumWithFourConstants.class, "B4R", "", Mode.INCLUDE).findAny()); + assertThat(exception).hasMessageContaining("No enum constant"); + } + + @Test + void invalidEndingRangeIsDetected() { + Exception exception = assertThrows(IllegalArgumentException.class, + () -> provideArguments(EnumWithFourConstants.class, "", "B4R", Mode.INCLUDE).findAny()); + assertThat(exception).hasMessageContaining("No enum constant"); + } + static class TestCase { - void methodWithCorrectParameter(EnumWithTwoConstants parameter) { + void methodWithCorrectParameter(EnumWithFourConstants parameter) { } void methodWithIncorrectParameter(Object parameter) { @@ -115,8 +175,11 @@ void methodWithoutParameters() { } } - enum EnumWithTwoConstants { - FOO, BAR + enum EnumWithFourConstants { + FOO, BAR, BAZ, QUX + } + + enum EnumWithNoConstant { } private > Stream provideArguments(Class enumClass, String... names) { @@ -124,12 +187,20 @@ private > Stream provideArguments(Class enumClass } private > Stream provideArguments(Class enumClass, Mode mode, String... names) { + return provideArguments(enumClass, "", "", mode, names); + } + + private > Stream provideArguments(Class enumClass, String from, String to, Mode mode, + String... names) { var annotation = mock(EnumSource.class); when(annotation.value()).thenAnswer(invocation -> enumClass); + when(annotation.from()).thenAnswer(invocation -> from); + when(annotation.to()).thenAnswer(invocation -> to); when(annotation.mode()).thenAnswer(invocation -> mode); when(annotation.names()).thenAnswer(invocation -> names); - when(annotation.toString()).thenReturn(String.format("@EnumSource(value=%s.class, mode=%s, names=%s)", - enumClass.getSimpleName(), mode, Arrays.toString(names))); + when(annotation.toString()).thenReturn( + String.format("@EnumSource(value=%s.class, from=%s, to=%s, mode=%s, names=%s)", enumClass.getSimpleName(), + from, to, mode, Arrays.toString(names))); var provider = new EnumArgumentsProvider(); provider.accept(annotation);