17

I want to parameterize my JUnit5 tests using three parameters: string, string and list<string>.

No luck so far when using @CsvSource, which is the most convenient way of passing params for my use case:

No implicit conversion to convert object of type java.lang.String to type java.util.List

The actual test is:

@ParameterizedTest()
@CsvSource(
  "2,1"
 )
fun shouldGetDataBit(first: Int, second: String, third: List<String>) {
    ...
}

Any idea if this is possible? I'm using Kotlin here but it should be irrelevant.

Grzegorz Piwowarek
  • 13,172
  • 8
  • 62
  • 93
  • You say "three parameters: `string`, `string` and `list`", but your example has `Int, String, List`. Were those intended to be consistent with each other? – M. Justin Mar 17 '23 at 22:47

3 Answers3

43

There is no reason to use a hack as suggested by StefanE.

At this point I'm pretty sure Junit5 Test Parameters does not support anything else than primitive types and CsvSource only one allowing mixing of the types.

Actually, JUnit Jupiter supports parameters of any type. It's just that the @CsvSource is limited to a few primitive types and String.

Thus instead of using a @CsvSource, you should use a @MethodSource as follows.

@ParameterizedTest
@MethodSource("generateData")
void shouldGetDataBit(int first, String second, List<String> third) {
    System.out.println(first);
    System.out.println(second);
    System.out.println(third);
}

static Stream<Arguments> generateData() {
    return Stream.of(
        Arguments.of(1, "foo", Arrays.asList("a", "b", "c")),
        Arguments.of(2, "bar", Arrays.asList("x", "y", "z"))
    );
}
Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
  • 4
    FYI: I updated the User Guide for JUnit 5.1 to include such an example. https://github.com/junit-team/junit5/commit/eb6e401fde681d7da649025391691ff5a45ca0f0 – Sam Brannen Oct 13 '17 at 14:43
  • 2
    The resulting documentation change is already visible in the latest snapshots: http://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-parameterized-tests-sources-MethodSource – Sam Brannen Oct 13 '17 at 14:52
  • 3
    Using MethodSource for this need is overkill. This is a common requirement for so much application. JUnit should provide a simpler way to inject a list of argument without having to deal with MethodSource. Something like @ListSource("value1" , "value2", "value3") would be perfect – soung Sep 08 '22 at 17:19
  • "`@CsvSource` is limited to a few primitive types and `String`" — I don't think that's quite true. I think it's limited to anything that [`DefaultArgumentConverter`](https://junit.org/junit5/docs/5.9.2/api/org.junit.jupiter.params/org/junit/jupiter/params/converter/DefaultArgumentConverter.html) can handle. That is primitives and their wrappers, String, `java.time` date/time types, a handful of other common types (`File`, `BigDecimal`, etc.), and anything with a single String factory method or a single String constructor. – M. Justin Mar 17 '23 at 22:02
3

Provide the third element as a comma separated string and the split the string into a List inside you test.

At this point I'm pretty sure Junit5 Test Parameters does not support anything else than primitive types and CsvSource only one allowing mixing of the types.

StefanE
  • 7,578
  • 10
  • 48
  • 75
  • Well, I'm doing this already - just hoped for something less "hacky" – Grzegorz Piwowarek Oct 12 '17 at 15:30
  • 1
    I am using only one value providing a JSON string instead for flexibility and no hacks needed. The Test parameter function is still in Beta, hopefully support for objects will come later on.. – StefanE Oct 12 '17 at 15:43
  • FYI: there is no need for a hack. Please see my answer for details. – Sam Brannen Oct 13 '17 at 13:58
  • "At this point I'm pretty sure Junit5 Test Parameters does not support anything else than primitive types" — I don't think that's quite true. I think it's limited to anything that [`DefaultArgumentConverter`](https://junit.org/junit5/docs/5.9.2/api/org.junit.jupiter.params/org/junit/jupiter/params/converter/DefaultArgumentConverter.html) can handle. That is primitives and their wrappers, String, `java.time` date/time types, a handful of other common types (`File`, `BigDecimal`, etc.), and anything with a single String factory method or a single String constructor. – M. Justin Mar 17 '23 at 22:05
0

If you really want to pass in a delimited list as part of the CSV (for instance, maybe it really does make the code look a lot cleaner), you can use the explicit conversion functionality of JUnit using a custom ArgumentConverter.

@CsvSource({
        "First,A;B",
        "Second,C",
        ",",
        "'',''"
})
@ParameterizedTest
public void testMethod(String string,
                       @ConvertWith(StringToStringListArgumentConverter.class)
                       List<String> list) {
    // ...
}

@SuppressWarnings("rawtypes")
public static class StringToStringListArgumentConverter
        extends TypedArgumentConverter<String, List> {
    protected StringToStringListArgumentConverter() {
        super(String.class, List.class);
    }

    @Override
    protected List convert(String source)
            throws ArgumentConversionException {
        if (source == null) {
            return null;
        } else if (source.isEmpty()) {
            return List.of();
        } else {
            return List.of(source.split(";"));
        }
    }
}

Fully generic list conversion

If you're planning on using the list conversion in multiple places with different element types in each place (e.g. List<String>, List<Integer>, List<LocalDate>), you could create a generic argument converter that delegates to a different argument converter to convert the different element types).

The following is an example that uses the Google Guava TypeToken class to determine the list element type, and the built-in internal JUnit DefaultArgumentConverter to perform the element conversions:

@CsvSource({
        "First,1;2",
        "Second,3",
        ",",
        "'',''"
})
@ParameterizedTest
public void testMethod(String string,
                       @ConvertWith(StringToListArgumentConverter.class)
                       List<Integer> list) {
    // ...
}

public static class StringToListArgumentConverter
        implements ArgumentConverter {
    @Override
    public Object convert(Object source, ParameterContext context)
            throws ArgumentConversionException {
        if (source != null && !(source instanceof String)) {
            throw new ArgumentConversionException("Source must be a string");
        }
        if (!List.class.isAssignableFrom(context.getParameter().getType())) {
            throw new ArgumentConversionException("Target must be a list");
        }
        String sourceString = (String) source;
        if (sourceString == null) {
            return null;
        } else if (sourceString.isEmpty()) {
            return List.of();
        }

        @SuppressWarnings("UnstableApiUsage")
        Class<?> elementType =
                TypeToken.of(context.getParameter().getParameterizedType())
                        .resolveType(List.class.getTypeParameters()[0])
                        .getRawType();

        return Arrays.stream(sourceString.split(";"))
                .map(s -> DefaultArgumentConverter.INSTANCE
                        .convert(s, elementType))
                .toList();
    }
}
M. Justin
  • 14,487
  • 7
  • 91
  • 130