Reputation: 6079
I wrote a mockup example to illustrate this without exposing anything confidential. It's a "dummy" example which does nothing, but the problem occurs in the test initialiser.
@RunWith(Parameterized.class)
public class ExampleParamTest
{
int ordinal;
List<String> strings;
public ExampleParamTest(int ordinal, String... strings)
{
this.ordinal = ordinal;
if (strings.length == 0)
{
this.strings = null;
}
else
{
this.strings = Arrays.asList(strings);
}
}
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{0, "hello", "goodbye"},
{1, "farewell"}
});
}
@Test
public void doTest() {
Assert.assertTrue(true);
}
}
Basically I have a test constructor which accepts multiple arguments for a local list variable and I want to populate this through an array initialiser. The test method will handle the local list variable correctly - I have removed this logic to simplify the test.
When I write this, my IDE has no complaints about syntax and the test class builds without any compile errors. However when I run it, I get:
doTest[0]:
java.lang.IllegalArgumentException: wrong number of arguments
at java.lang.reflect.Constructor.newInstance(Unknown Source)
doTest[1]:
java.lang.IllegalArgumentException: argument type mismatch
at java.lang.reflect.Constructor.newInstance(Unknown Source)
What exactly has gone wrong here, and how do I correctly use this pattern?
Upvotes: 8
Views: 8216
Reputation: 5333
The workaround given by @marcphillip in this JUnit feature request works great for now:
@ParameterizedTest
@CsvSource({"1,a", "1,a,b,c"})
void testAbc(int arg1, @AggregateWith(VarargsAggregator.class) String... elements) {
System.out.println(Arrays.toString(elements));
}
static class VarargsAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
Class<?> parameterType = context.getParameter().getType();
Preconditions.condition(parameterType.isArray(), () -> "must be an array type, but was " + parameterType);
Class<?> componentType = parameterType.getComponentType();
return IntStream.range(context.getIndex(), accessor.size())
.mapToObj(index -> accessor.get(index, componentType))
.toArray(size -> (Object[]) Array.newInstance(componentType, size));
}
}
Update: I added this tweak to give an empty array when there is nothing provided after the last comma: Update2: I realized this is not needed: Just leave off the trailing comma (which I'd assumed was mandatory) in order to pass in an empty vararg array.
if (result.length == 1 && result[0] == null) {
return Array.newInstance(componentType, 0);
}
return result;
Upvotes: 0
Reputation: 114807
Can't test it right now but I guess, if you invoke a method or a constructor with variable arguments, you have to invoke it with an array instead of a variable list of values.
If I'm right, then this should work:
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{0, new String[]{"hello", "goodbye"}},
{1, new String[]{"farewell"}}
});
}
Some explanation
On source code level, we can write
test = ExampleParamTest(0, "one", "two");
The compiler will convert this to an array of Strings. JUnit uses the reflection and invocation API, and from this perspective, the constructors signature is
public ExampleParamTest(int i, String[] strings);
So to invoke the constructor - and that's what JUnit is doing internally - you have to pass an integer and a String array.
Upvotes: 12