128

How can I write a parameterized test with two arguments in JUnit 5 jupiter? The following does not work (compile error):

@ParameterizedTest
@ValueSource(strings = { "a", "b", "foo" })
@ValueSource(ints = { 1, 2, 3 })
public void test(String arg1, int arg2) {
    // ...
}

4 Answers 4

210

Here are two possibilities to achieve these multi argument test method calls.

The first (testParameters) uses a CsvSource to which you provide a comma separated list (the delimiter is configurable) and the type casting is done automatically to your test method parameters.

The second (testParametersFromMethod) uses a method (provideParameters) to provide the needed data.

@ParameterizedTest
@CsvSource({"a,1", "b,2", "foo,3"})
public void testParameters(String name, int value) {
    System.out.println("csv data " + name + " value " + value);
}

@ParameterizedTest
@MethodSource("provideParameters")
public void testParametersFromMethod(String name, int value) {
    System.out.println("method data " + name + " value " + value);
}

private static Stream<Arguments> provideParameters() {
    return Stream.of(
            Arguments.of("a", 1),
            Arguments.of("b", 2),
            Arguments.of("foo", 3)
    );
}

The output of these test methods are:

Running ParameterTest
csv data a value 1
csv data b value 2
csv data foo value 3
method data a value 1
method data b value 2
method data foo value 3
Sign up to request clarification or add additional context in comments.

Thanks a lot. Is there a way to test the cross product ("a,1","a,2","a,3","b,1","b,2",...) without writing every combination?
For this the method approach will do the job. In provideParameters you could build up every combination you want.
@ruediste There is already an issue about that: github.com/junit-team/junit5/issues/1427.
16

Best Practices Parameterized Test!!

Consider a simple function to test:

public static boolean isDivisibleBy(int dividend, int divisor){
    return dividend % divisor == 0;
}

When generating JUnit5 Parameterized Test case:

  1. Use @DisplayName annotation with a message.

  2. Use @ParameterizedTest with name argument.

  3. Use @ParameterizedTest(name = " with {0}..{N} ") to point to @CsvSource's values.

  4. Use @CsvSource({"key1,value1", "key2,value2", "key3,value3", .. }) without worry of conversion. This mechanism is based on implicit converters that can interpret a String into another type, such as int. This particular example may not seem that impressive, but it works with a lot of very useful common types, such as File, Path, BigDecimal, Date, Time, enum and URL.

  5. Don't use @Test along with @ParameterizedTest.

Example:

@DisplayName("Divide Test Cases 😎")
@ParameterizedTest(name = "{0} is multiple of {1}")
@CsvSource({"2,2", "4,2", "7,3"})
void shouldReturnTrueForVariousDivisiblePairs(int givenDividend, int givenDivisor) {
    assertTrue(isDivisibleBy(givenDividend, givenDivisor));
}

Pretty Output Log:

Output

Very nice. This is much more verbose than just writing tests with no description.
1

Here is a solution using Kotlin language:

class MyTests {
    @ParameterizedTest
    @MethodSource("generateArgs")
    fun myTest(myArg: Pair<String, Int>) {
        val (name, age) = myArg // Destructuring; could instead use myArg.first and myArg.second
        println("$name $age")
    }

    companion object {
        @JvmStatic
        fun generateArgs() = listOf (
            "A" to 1,
            "B" to 2,
            "C" to 3
        )
    }
}

Comments

0

You could also use DynamicTest (those have added benefit of being checked by compiler, so you won't pass in incorrect type), this will generate three tests with a set of two parameters:

@TestFactory
Stream<DynamicTest> test() {
    return Map.of(
        "a", 1,
        "b", 2,
        "foo", 3
    ).entrySet().stream().map(
        e -> DynamicTest.dynamicTest(
            "test %s, %d".formatted(e.getKey(), e.getValue()),
            () -> // some assertions on e.getKey and e.getValue
        )
    );
}

Parametrized Tests are good if you operate on Strings only, or test full enums (without filtering, because if you filter you again use strings to do it - fails at runtime, with dynamic test it would fail during compile time)

For dynamic tests you can use any source you like, above example is with map, but you could use something more readable like:

@TestFactory
Stream<DynamicTest> test() {
    record TestData(String arg1, int arg2) {}
    return Stream.of(
        new TestData("a", 1),
        new TestData("b", 2),
        new TestData("foo", 3)
    ).map(
    e -> DynamicTest.dynamicTest(
           "test %s, %d".formatted(e.getKey(), e.getValue()),
           () -> // some assertions on e.getKey and e.getValue
       )
    );
}

Comments

Your Answer

Draft saved
Draft discarded

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.