Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce from and to attributes on @EnumSource (#4185) #4221

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
26 changes: 24 additions & 2 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
17 changes: 17 additions & 0 deletions documentation/src/test/java/example/ParameterizedTestDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Choose a reason for hiding this comment

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

Suggested change
}
assertFalse(EnumSet.of(ChronoUnit.SECONDS).contains(unit));
}

// end::EnumSource_range_example[]

// tag::EnumSource_exclude_example[]
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
Expand All @@ -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" })

Choose a reason for hiding this comment

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

this test exemple will depend on the JDK. So, if the order is change, it can impact your test. is it not better to use a home made enum for the test exemple ?

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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ protected Stream<? extends Arguments> provideArguments(ExtensionContext context,

private <E extends Enum<E>> Set<? extends E> getEnumConstants(ExtensionContext context, EnumSource enumSource) {
Class<E> 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);

Choose a reason for hiding this comment

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

What happen if from > to or if from == to ?
It maybe need to be specify somewhere in the doc, and have a test on this cases

}

@SuppressWarnings({ "unchecked", "rawtypes" })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
* attribute. Otherwise, the declared type of the first parameter of the
* {@code @ParameterizedTest} method is used.
*
* <p>The set of enum constants can be restricted via the {@link #names} and
* {@link #mode} attributes.
* <p>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
Expand All @@ -63,6 +63,8 @@
* first parameter of the {@code @ParameterizedTest} method is used.
*
* @see #names
* @see #from
* @see #to

Choose a reason for hiding this comment

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

maybe also add it to the mode() method javadoc

* @see #mode
*/
Class<? extends Enum<?>> value() default NullEnum.class;
Expand All @@ -71,16 +73,48 @@
* The names of enum constants to provide, or regular expressions to select
* the names of enum constants to provide.
*
* <p>If no names or regular expressions are specified, all enum constants
* declared in the specified {@linkplain #value enum type} will be provided.
* <p>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.
*
* <p>If {@link #from} or {@link #to} are specified, the elements in names must
* fall within the range defined by {@link #from} and {@link #to}.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -34,54 +36,56 @@ 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 });
}

@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
Expand All @@ -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) {
Expand All @@ -115,21 +175,32 @@ void methodWithoutParameters() {
}
}

enum EnumWithTwoConstants {
FOO, BAR
enum EnumWithFourConstants {
FOO, BAR, BAZ, QUX
}

enum EnumWithNoConstant {
}

private <E extends Enum<E>> Stream<Object[]> provideArguments(Class<E> enumClass, String... names) {
return provideArguments(enumClass, Mode.INCLUDE, names);
}

private <E extends Enum<E>> Stream<Object[]> provideArguments(Class<E> enumClass, Mode mode, String... names) {
return provideArguments(enumClass, "", "", mode, names);
}

private <E extends Enum<E>> Stream<Object[]> provideArguments(Class<E> 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);
Expand Down
Loading