Java泛型-方法不适用于Mockito生成的存根

Kre*_*ase 5 java generics mockito

我有一个ThingsProvider要尝试使用Mockito(简化版)测试的界面,定义如下:

interface ThingsProvider {
    Iterable<? extends Thing> getThings()
}
Run Code Online (Sandbox Code Playgroud)

现在,当我要使用Mockito对其进行测试时,我正在执行以下操作(再次简化该问题):

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

编译错误信息: 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>)

现在,纯粹是为了进行测试,我将最后一行更改为

when(thingsProvider.getThings()).thenReturn((List)things); // HAHA take THAT generics!
Run Code Online (Sandbox Code Playgroud)

...但是在非测试代码中这样做显然是不好的。

我的问题:

  1. 为什么这是一个错误?我显然返回的是extend对象Thing,这是接口所期望的。
  2. 有解决这个问题的更好方法吗?也许以不同的方式定义我的界面?到目前为止,我还没有在测试之外遇到这个问题...

在#2上-我不简单返回的主要原因Iterable<Thing>是,有几种不同的扩展,其中具体类型中埋藏着返回特定子类型的东西,而我最终Type mismatch: cannot convert from Iterable<MagicalThing> to Iterable<Thing>遇到了类似的问题-也许解决方案是一种更好的解决方案解决这个问题?


对于那些不太熟悉Mockito的人,下面是更纯粹的Java版本#1:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Zho*_*gYu 4

正如您所观察到的,返回类型中的通配符对于子类实现非常方便。

不过,这对该方法的调用者来说并没有多大区别;Iterable<Thing>如果您愿意,可以将其更改为;它在 javadoc 上更简单,但以牺牲子类实现者为代价。如有必要,子类可以进行暴力转换,例如List<MyThing> => Iterable<Thing>,通过擦除。


您的问题的原因是通配符捕获;基本上每个表达式在计算上下文表达式之前都会先进行通配符捕获。在方法调用中

    foo( arg )
Run Code Online (Sandbox Code Playgroud)

arg在方法适用性/重载/推理完成之前,始终首先捕获通配符。在你的情况下,更通用的类型Iterable<? extends Thing>丢失了,变成了Iterable<CAP#>

通常,通配符捕获不会造成任何问题;但 Mockito 语义并不常见。


第一个解决方案是避免类型推断;显式提供类型参数

Mockito.<Iterable<? extends Thing>>when(...
Run Code Online (Sandbox Code Playgroud)

或者正如 Delimanolis 建议的那样,使用目标类型来限制推理(在 java8+ 中)

OngoingStubbing<Iterable<? extends Thing>> stub =  when(...
Run Code Online (Sandbox Code Playgroud)

看来 lambda 推断可能对这种情况有帮助

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

whenX(thingsProvider::getThings).thenReturn(things);
// T = Iterable<? extends Thing>
Run Code Online (Sandbox Code Playgroud)

而且 - 如果你的界面足够简单,只需直接实现它而不是模拟它:)

List<Thing> things = ...;

ThingsProvider thingsProvider = ()->things;
Run Code Online (Sandbox Code Playgroud)