junit5 icon indicating copy to clipboard operation
junit5 copied to clipboard

Introduce `@FieldSource` for parameterized tests

Open ktkopone opened this issue 5 years ago • 17 comments

Overview

Introduce @FieldSource argument source for parameterized tests.

The feature is analogous to the existing @MethodSource support, except that the value of a named field is used as the source of arguments instead of the value returned by a factory method.

See https://github.com/junit-team/junit5/issues/2014#issuecomment-1523242161 and https://github.com/junit-team/junit5/issues/2014#issuecomment-1523333359 for examples.

Deliverables

  • [x] Introduce @FieldSource and FieldArgumentsProvider.
    • [x] Reject fields with Stream values.
    • [x] Support Supplier<T> fields where T is a type supported for @MethodSource.
    • [ ] Write unit and integration tests for preconditions, edge cases, etc.
    • [ ] Provide examples, etc. in class-level Javadoc for @FieldSource.
  • [ ] Document in the User Guide.
  • [ ] Document in the Release Notes.

Original Issue Description

I understand that for memory/performance you probably want to recommend everyone use a Stream-returning method and @MethodSource, but given that an @ArraySource would be easier to use without having to convert all test data into methods, this would be a nice-to-have.

ktkopone avatar Sep 18 '19 22:09 ktkopone

Have you tried @ValueSource?

marcphilipp avatar Sep 19 '19 05:09 marcphilipp

A @MethodSource factory method doesn't have to return a Stream. It can also return an array. So why don't you just do that?

Also, how would an @ArraySource annotation be used?

Would it reference the name of a field that has an array type?

If not, then @ValueSource likely already meets your needs as @marcphilipp already pointed out.

sbrannen avatar Sep 19 '19 10:09 sbrannen

Apologies, I had a longer writeup with context but tried to pair it down. In my case I have an array variable of strings that I want to reuse as data for multiple parameterized tests (hence I don't want to just hardcode it in the @ValueSource strings parameter). If there's a way to pass an array variable to @ValueSource without running into Attribute value must be constant, I don't know it.

It's good to know that @MethodSource can return arrays. However, it is still a pain point that I'll have to write a custom method just to return data which I have stored in array vars. I could skip on having the array variable at all but I like being able to define all my test data together as properties of a TestData class, and I'm sure there exist scenarios where there would be good reasons to store in arrays rather than only being returned via methods.

So given that in many use cases the data will live in an array field, it would be convenient if the user could pass this via @ArraySource(arrayVar) without having to write a dummy method for each field.

ktkopone avatar Sep 19 '19 17:09 ktkopone

Based on your feedback, what you are effectively requesting is a @FieldSource annotation analogous to the existing @MethodSource. In other words, there is nothing specific in your request that pertains only to arrays. I will therefore update the title of the issue accordingly.

sbrannen avatar Sep 19 '19 20:09 sbrannen

Tentatively slated for the 5.6 backlog for team discussion

sbrannen avatar Sep 19 '19 20:09 sbrannen

In the meantime, this might be a little heavy on the ceremony but works:

@Retention(RetentionPolicy.RUNTIME)
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
@interface MyArraySource {}

@ParameterizedTest
@MyArraySource
void palindromes(String candidate) {
	assertTrue(StringUtils.isPalindrome(candidate));
}

marcphilipp avatar Sep 20 '19 06:09 marcphilipp

I just hit this issue - @marcphilipp's workaround is only useful if you don't intend on re-using the array of values in your code. If you want to access the array in your code you either have to duplicate the initialisation list somewhere in an array initialiser, or else you have to use reflection to get access to the annotation on your meta-annotated annotation. A @FieldSource would be nice; otherwise a @MethodSource can probably be used as a workaround.

kriegfrj avatar Aug 20 '20 06:08 kriegfrj

This issue has been automatically marked as stale because it has not had recent activity. Given the limited bandwidth of the team, it will be automatically closed if no further activity occurs. Thank you for your contribution.

stale[bot] avatar Jun 19 '22 19:06 stale[bot]

This issue has been automatically closed due to inactivity. If you have a good use case for this feature, please feel free to reopen the issue.

stale[bot] avatar Jul 11 '22 20:07 stale[bot]

private static final String[] TREATMENTS = {"1", "2"};
@ParameterizedTest
@ValueSource(strings = TREATMENTS)
public void test(String treatment) {}

This gives a "Attribute value must be constant" error. Screen Shot 2022-11-11 at 4 06 36 PM

How can i re-open this issue?

ahertel avatar Nov 11 '22 23:11 ahertel

@ahertel, if you want to use the contents of a static array as the source for a parameterized test, you will currently have to use a factory method as in the following example.

private static final String[] TREATMENTS = {"1", "2"};

@ParameterizedTest
@MethodSource("treatments")
void test(String treatment) {}

static String[] treatments() {
    return TREATMENTS;
}

sbrannen avatar Nov 13 '22 17:11 sbrannen

@sbrannen thanks Sam. I discovered that this also works :)

private static final String T1 = "T1";
private static final String T2 = "T2";

@ParameterizedTest
@ValueSource(strings = {T1, T2})
public void test(String treatment) {}

After learning more about JUnit 5's offerings, I ended up just making an Enum and using @EnumSource

ahertel avatar Nov 14 '22 07:11 ahertel

Lately I've noticed that there are times when it would be nice to be able to avoid the ceremony of creating a factory method for use with @MethodSource, and that brought me back to the idea of @FieldSource.

As a result, I have spiked an implementation which can be viewed here: https://github.com/sbrannen/junit5/commit/e74592a937eeba0415611831c740458628eb63de

In light of that, I am reopening this issue.

sbrannen avatar Apr 26 '23 11:04 sbrannen

For those who missed the link in my previous comment, the following demo tests work.

class FieldSourceDemo {

	private static final String[] arrayOfStrings = { "apple", "banana" };

	private static final Object listOfStrings = List.of("apple", "banana");

	private static final List<String> reversedStrings = List.of("banana", "apple");

	private static final Stream<Arguments> namedArguments = Stream.of(//
		arguments(named("Apfel", "apple")), //
		arguments(named("Banane", "banana"))//
	);

	@ParameterizedTest
	@FieldSource // default field name
	void arrayOfStrings(String fruit) {
		assertFruit(fruit);
	}

	@ParameterizedTest
	@FieldSource("listOfStrings")
	void singleFieldSource(String fruit) {
		assertFruit(fruit);
	}

	@ParameterizedTest
	@FieldSource({ "listOfStrings", "reversedStrings" })
	void multipleFieldSources(String fruit) {
		assertFruit(fruit);
	}

	@ParameterizedTest
	@FieldSource
	void namedArguments(String fruit) {
		assertFruit(fruit);
	}

	@Nested
	@TestInstance(Lifecycle.PER_CLASS)
	class NestedTests {

		// Non-static field
		final List<String> strings = List.of("apple", "banana");

		@ParameterizedTest
		@FieldSource("strings")
		void nonStaticFieldSource(String fruit) {
			assertFruit(fruit);
		}

	}

	static void assertFruit(String fruit) {
		assertTrue("apple".equals(fruit) || "banana".equals(fruit));
	}

}

sbrannen avatar Apr 26 '23 11:04 sbrannen

Update: the latest commit in the PoC adds support for referencing static fields in external classes via a fully-qualified field name, as demonstrated below.

@ParameterizedTest
@FieldSource("example.Utils#fruits")
void externalField(String fruit) {
	assertFruit(fruit);
}

sbrannen avatar Apr 26 '23 12:04 sbrannen

Team decision: Investigate more closely for 5.11. Reject fields with Stream values; support Supplier<T> fields where T is a type supported for @MethodSource, though.

marcphilipp avatar May 05 '23 10:05 marcphilipp

Update

The latest work in my feature branch is effectively feature complete except documentation in the user guide and extensive tests.

sbrannen avatar Aug 06 '23 12:08 sbrannen

Team decision: Investigate more closely for 5.11. Reject fields with Stream values; support Supplier<T> fields where T is a type supported for @MethodSource, though.

While writing exhaustive tests for this feature, I determined that we should also reject fields with Iterator values, since an Iterator would also be consumed after the first retrieval of the field and therefore invalid for potential subsequent retrievals.

sbrannen avatar Apr 13 '24 14:04 sbrannen

Update

The latest work in my feature branch is now feature complete and fully tested.

All that remains is documentation in the user guide and release notes.

sbrannen avatar Apr 13 '24 16:04 sbrannen

Huh nice timing to search on a feature and find it just implemented. :-)

I'd just like to say that what I really love about this is that it will make data look like data, and leave methods to implement behaviour. IMHO this much more than just a minor cosmentic difference, I think it will really accelerate "visually parsing" and understanding tests.

lukeu avatar Apr 16 '24 08:04 lukeu