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

A way to provide the arguments "name" without actually having it as an argument. #2301

Closed
xenoterracide opened this issue May 15, 2020 · 17 comments · Fixed by #2521
Closed

Comments

@xenoterracide
Copy link

xenoterracide commented May 15, 2020

So I have a parameterized test, and each parameter is a List of Beans where each bean represents a line in a csv file (opencsv). What I want for the actual "parameterized name" is the filename.

I'm thinking something like return

Arguments.of( Named.by(file.getName()), new ArrayList<>() );

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void test( List<AlgorithmVerification> verifications )

Obviously the workaround is to simply pass it as the first parameter and include it as an unused argument. I just have a dislike of unused arguments.

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void test( String filename, List<AlgorithmVerification> verifications )
@juliette-derancourt
Copy link
Member

I had the exact same use case not long ago, and I ended up passing the name as an argument like you did. I agree that it's not ideal...

That being said, I'm not sure how your solution whould deal with the name attribute of @ParameterizedTest? Would the name provided in Arguments replace it? Would you be able to insert it in the name with a placeholder like the other arguments?

@xenoterracide
Copy link
Author

I think in the proposed solution it would replace it, though you could also do that latter, I'm not certain on the right answer there. TBH I'm not even certain the proposed answer is the right answer.

@juliette-derancourt
Copy link
Member

Personally I think I'd prefer the latter.
But I'm not sure it would be useful outside of this (pretty rare) use case... Unless you see other ones?

TBH I'm not even certain the proposed answer is the right answer.

Well I guess there is no right answer anyway 🤓

@juliette-derancourt
Copy link
Member

I think the request in #1154 would also solve your problem, wouldn't it?

@xenoterracide
Copy link
Author

xenoterracide commented May 22, 2020

no, seems completely unrelated, since mine was about having to receive a parameter that I'm not using at all, other than to go into the test name. That would still be true even if the parser was improved. #1154 is still a great idea though. Thing is, in my example filename isn't in the array at all, it serves no real purpose other than to describe the file that I'm reading

@xenoterracide
Copy link
Author

xenoterracide commented May 22, 2020

Actually I might have described my proposed solution poorly, a better Api might be something like (obviously some pseudocode)

Arguments.of( Argument.of(filename, passAsArgument = false ))

then it wouldn't actually pass that first argument to the method signature, but it would still be passed to the description.

this is a pretty minor problem though.

@marcphilipp
Copy link
Member

How about a simple Named<T> object that wraps the argument?

@xenoterracide
Copy link
Author

personally not opposed

@juliette-derancourt
Copy link
Member

How about a simple Named<T> object that wraps the argument?

I like the idea! 👍

@sbrannen sbrannen added this to the 5.8 Backlog milestone Aug 7, 2020
@sbrannen
Copy link
Member

sbrannen commented Aug 7, 2020

Tentatively slated for 5.8 Backlog to be considered in conjunction with #2375.

@dmitry-timofeev
Copy link
Contributor

Just in case, there also was a previous discussion on this issue, where it had been considered to make Arguments work like a builder, with a method that sets an optional name and returns an Arguments instance with a name set (#1309). E.g., arguments("foo", 3).named("bar").

Named is also nice, giving an opportunity to put the argument set name first/last up to the user preference 👍

@ghost
Copy link

ghost commented Sep 24, 2020

Just had a similar problem caused by useless descriptions generated for function-type arguments. One solution right now is something like this:

@Immutable
public abstract class ArgumentsWithDescription implements Arguments {

    public static ArgumentsWithDescription of(String description, Object... arguments) {
        return ImmutableArgumentsWithDescription.of(
            description,
            List.of(arguments)
        );
    }

    @Parameter
    public abstract String description();

    @Parameter
    public abstract List<Object> arguments();

    @Override
    public Object[] get() {
        return ImmutableList.builder()
            .add(description())
            .addAll(arguments())
            .build()
            .toArray();
    }
}

and then define the test as:

@ParameterizedTest(name = "{0}")
    @MethodSource("getParametersForTest")
    public void myTest(String testDescription, Supplier<String> arg1, arg2, etc.) {}

Finally, the method providing the parameters:

private static List<ArgumentsWithDescription> getParametersForTest() {
        return List.of(
            ArgumentsWithDescription.of("My test description", (Supplier<String>)() -> "meh", arg2, etc.)
        );
    }

This seems already pretty fine. The only real benefit here would be for JUnit to support using toString() from the arguments class instead. However, it's a pretty minor benefit.

Note that this solution obviously allows you to support any kind of description generation. You could easily extend it to use String.format etc. and even support inlining arguments via {1} etc.

@marcphilipp marcphilipp modified the milestones: 5.8 Backlog, 5.8 M1 Sep 25, 2020
@marcphilipp
Copy link
Member

marcphilipp commented Sep 25, 2020

Team decision: Add a Named<T> interface in junit-jupiter-api and add automatic support for injecting the contained payload into parameterized methods directly.

public interface Named<T> {
	static <T> Named<T> of(String name, T payload) {
		return new Named<T>() {
			@Override
			public String getName() {
				return name;
			}
			@Override
			public T getPayload() {
				return payload;
			}
			@Override
			public String toString() {
				return name;
			}
		};
	}
	String getName();
	T getPayload();
}

@thomasdarimont
Copy link

thomasdarimont commented Oct 2, 2020

This could be really useful when replicating Go style table-driven tests with DynamicTests and local records from Java 15.

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

class TableDrivenTest {

    @TestFactory
    Stream<DynamicTest> tableDrivenTestFromStream() {

        record TestCase(String name, int a, int b, int sum) {

            public void check() {
                assertEquals(sum, a + b, name);
            }
        }

        var testCases = Stream.of(
                new TestCase("test1", 1, 2, 3),
                new TestCase("test2", 2, 2, 4),
                new TestCase("test3", 4, 2, 6)
        );

        return DynamicTest.stream(testCases, TestCase::name, TestCase::check);
    }
}

See: https://twitter.com/thomasdarimont/status/1312100991127285760

With the Named interface from above, one could reduce the plumbing for this a bit.

Btw. if the DisplayName annotation were a bit more flexible one could do something like this:

...
        record TestCase(String name, int a, int b, int sum) {
            @Test
            @DisplayName("$name: $a + $b = $sum") 
            public void check() {
                assertEquals(sum, a + b, name);
            }
        }
...

Here is a gist with some more examples: https://gist.github.com/thomasdarimont/1650ab4d914072bb32d32b58a9ccc571

@FlorianCousin
Copy link

What if we have several parameters in a test as follows ?

  @ParameterizedTest
  @ArgumentsSource(DataProvider.class)
  void test(String firstInput, String secondInput) {
    // A test
  }

For such a test, I would get my parameters as follows :

  Stream.of(arguments("first input", "second input"));

It seems that I cannot write

  Stream.of(arguments(Named.of("cool name", "first input", "second input")));

Is there any trick I missed ?

@marcphilipp
Copy link
Member

Named allows you to override the name of one argument.

Hence, you could do

Stream.of(arguments(named("cool name", "first input"), "second input"));

and then use the name attribute in order to use that name to describe the entire invocation:

@ParameterizedTest(name = "{0}")
@ArgumentsSource(DataProvider.class)
void test(String firstInput, String secondInput) {
	// A test
}

@FlorianCousin
Copy link

I see, thank you @marcphilipp.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants