Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Generics - method not applicable to Mockito generated stub

I've got a ThingsProvider interface I'm trying to test with Mockito, (simplified version) defined as follows:

interface ThingsProvider {
    Iterable<? extends Thing> getThings()
}

Now when I'm going to test it with Mockito, I'm doing the following (again, simplified for the question):

ThingsProvider thingsProvider = mock(ThingsProvider.class);
List<Thing> things = Arrays.asList(mock(Thing.class));
when(thingsProvider.getThings()).thenReturn(things); // PROBLEM IS HERE

Compile error message: The method thenReturn(Iterable<capture#11-of ? extends Thing>) in the type OngoingStubbing<Iterable<capture#11-of ? extends Thing>> is not applicable for the arguments (List<Thing>)

Now, purely for getting the test going, I'm changing the last line to

when(thingsProvider.getThings()).thenReturn((List)things); // HAHA take THAT generics!

... but this would clearly be bad to do in non-testing code.

My question(s):

  1. Why is this an error? I'm clearly returning objects that extend Thing, which is what the interface expects.
  2. Is there a better way of solving this? Perhaps defining my interface differently? I haven't run into this issue outside of testing so far...

On #2 - The main reason I'm not simply returning Iterable<Thing> is that there are several different extensions where the concrete types have things buried in them that return specific subtypes, and I end up with issues like Type mismatch: cannot convert from Iterable<MagicalThing> to Iterable<Thing> - maybe the solution is a better way to fix this issue?


For those of you less familiar with Mockito, a more pure Java version of #1 is below:

public static void main(String...args) {
    List<Integer> ints = Arrays.asList(1,2,3);
    blah(ints);

    Foo<Number> foo1 = new Foo<Number>();
    foo1.bar(ints);  // This works

    Foo<? extends Number> foo2 = new Foo<Number>();
    foo2.bar(ints);  // NO COMPILEY!
} 

private static void blah(List<? extends Number> numberList) {
    // something
}

public static class Foo<T> {
    public Object bar(List<? extends T> tList) {
        return null;
    }
}
like image 382
Krease Avatar asked Jun 23 '15 00:06

Krease


2 Answers

Wildcard in return type is very convenient for subclass implementations, as you've observed.

It doesn't make much difference to the callers of the method though; you may change it to Iterable<Thing> if you'd like to; it's simpler on javadoc, at the expense of subclass implementers. Subclass can do brute cast if necessary, e.g. List<MyThing> => Iterable<Thing>, thanks to erasure.


The reason for your problem is wildcard capture; basically every expression goes though wildcard capture first before the contextual expression is evaluated. In a method invocation

    foo( arg )

arg is always wildcard-captured first, before method applicability/overloading/inference are done. In your case, the more general type Iterable<? extends Thing> is lost, becomes Iterable<CAP#>.

Usually, wildcard capture does not pose any problem; but Mockito semantics is nothing usual.


The first solution is to avoid type inference; explicitly supply type arguments instead

Mockito.<Iterable<? extends Thing>>when(...

Or as Delimanolis suggested, use a target type to restrict inference (in java8+)

OngoingStubbing<Iterable<? extends Thing>> stub =  when(...

It also appears that lambda inference may be helpful for this case

static <T> OngoingStubbing<T> whenX( Supplier<T> sup )
{    
    return Mockito.when( sup.get() );
}

whenX(thingsProvider::getThings).thenReturn(things);
// T = Iterable<? extends Thing>

And - if your interface is simple enough, just directly implement it instead of mock it :)

List<Thing> things = ...;

ThingsProvider thingsProvider = ()->things;
like image 200
ZhongYu Avatar answered Nov 13 '22 09:11

ZhongYu


Refactor into two steps

OngoingStubbing<Iterable<? extends Thing>> stub =  when(thingsProvider.getThings());
stub.thenReturn(things);

or use what bayou.io suggested

Mockito.<Iterable<? extends Thing>>when(thingsProvider.getThings())
like image 33
4 revs Avatar answered Nov 13 '22 09:11

4 revs