B. Stackhouse
B. Stackhouse

Reputation: 597

JUnit 5 and Arguments.of() with functions

Writing a JUnit 5 parameterized test and need to pass functions to the test using Arguments.of(), but there are 2 compile errors that I don't know how to fix. Any help would be appreciated.

  1. The method of(Object...) in the type Arguments is not applicable for the arguments (boolean, String::length)
  2. The target type of this expression must be a functional interface

public static Stream<Arguments> some() {
    return Stream.of(Arguments.of(true, String::length));
}

@ParameterizedTest
@MethodSource
public <T> void some(final T input, final Function<String, Integer> length) {
}

The following works as expected.

public void sample() {
    some(true, String::length);
}

Upvotes: 7

Views: 7447

Answers (4)

Tim van der Leeuw
Tim van der Leeuw

Reputation: 406

I like @Miguel Alorda's solution of the wrapper function, however what also works is casting to the right type:

Arguments.of((Function<String,Integer>) String::length)

Or with an inline lambda such as this no-argument lambda:

Arguments.of((Supplier<String>) ()-> "Supply World")

BTW: You don't actually need the return-type, but you do need the type of the operand of the Function<T,R> otherwise the compiler cannot determine th method-reference to the length() function.

So the following compiles:

Arguments.of((Function<String,?>) String::length)

But this doesn't:

Arguments.of((Function<?,?>) String::length)

Upvotes: 2

Miguel Alorda
Miguel Alorda

Reputation: 692

I liked @adrian-redgers solution, but I think overloading a method for each signature needed is a bit overkill.

You only really need to convert the functional interface to an object. So the solution I implemented was:

/**
 * Helps to use {@link org.junit.jupiter.params.provider.Arguments#of(Object...)}, as functional
 * interfaces cannot be converted into an object directly.
 */
public class ArgumentsWrapper {

    private ArgumentsWrapper() {
        throw new IllegalStateException(
                ArgumentsWrapper.class + " util class cannot be instantiated");
    }

    public static <T, U> Function<T, U> wrap(Function<T, U> function) {
        return function;
    }
}

Then, it can be used as:

 public static Stream<Arguments> testMapAlarmTypeConfigWithLanguage() {
    return Stream.of(
        // Statically imported ArgumentsWrapper#wrap
        Arguments.of(null, wrap(AlarmTypeConfig::getNameInEnglish)),
        Arguments.of("en-us", wrap(AlarmTypeConfig::getNameInEnglish)),
        Arguments.of("es-es", wrap(AlarmTypeConfig::getNameInSpanish)));
}

Upvotes: 2

Adrian Redgers
Adrian Redgers

Reputation: 394

Wrap the arguments in a helper method
Similar to the answer "wrap it in a class", but possibly less intrusive, is to use a helper method to pass the functional interface as a java.lang.Object.

For example, the first raw method reference, Math::ciel, in this parameterized test:

@ParameterizedTest
@MethodSource("testCases")
void shouldExerciseMethod(Function<Double, Double> method, Double expected) {
    assertEquals(expected, method.apply(1.5d), 1.0E-8d);
}
    
static Stream<Arguments> testCases() {
    return Stream.of(Arguments.of(Math::ceil, 2.0d),
                     Arguments.of(Math::floor, 1.0d));
}

causes this compilation error:

java: method of in interface org.junit.jupiter.params.provider.Arguments cannot be applied to given types;
  required: java.lang.Object[]
  found: Math::ceil,double
  reason: varargs mismatch; java.lang.Object is not a functional interface

which you can get around by passing the arguments through a helper method:

static <T, U> Arguments args(Function<T, U> method, U expected) {
    return Arguments.of(method, expected);
}

so:

static Stream<Arguments> testCases() {
    return Stream.of(args(Math::ceil, 2.0d), 
                     args(Math::floor, 1.0d));
}

My attempts to make the idiom more general using varargs failed with variations on the same error, so I have ended up overloading it whenever I need another signature.

Upvotes: 1

B. Stackhouse
B. Stackhouse

Reputation: 597

The function needs to be wrapped in a class.

public static class P {

    private final Function<String, Integer> mFunction;

    public P(final Function<String, Integer> function) {
        mFunction = function;
    }

    public Function<String, Integer> function() {
        return mFunction;
    }
}

public static Stream<Arguments> some() {
    return Stream.of(Arguments.of(3, "abc", new P(String::length)));
}

@ParameterizedTest
@MethodSource
public <T> void some(final int expect, final String input, final P p) {
    assertEquals(expect, p.function().apply(input));
}

Upvotes: -1

Related Questions