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

Java Generics - method not applicable to Mockito generated stub

我有一个 ThingsProvider 接口,我正在尝试使用 Mockito 进行测试,(简化版)定义如下:

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

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

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

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

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

我的问题:

  1. 为什么这是一个错误?我显然 returning 扩展了 Thing 的对象,这是界面所期望的。
  2. 有没有更好的方法解决这个问题?也许以不同的方式定义我的界面?到目前为止,除了测试之外,我还没有 运行 解决这个问题...

关于 #2 - 我不简单地 returning Iterable<Thing> 的主要原因是有几个不同的扩展,具体类型中隐藏了 return特定的子类型,我最终遇到了 Type mismatch: cannot convert from Iterable<MagicalThing> to Iterable<Thing> 之类的问题 - 也许解决方案是解决 这个 问题的更好方法?


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

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;
    }
}

重构为两步

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

或使用建议的

Mockito.<Iterable<? extends Thing>>when(thingsProvider.getThings())

如您所见,return 类型中的通配符对于子类实现非常方便。

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


你的问题的原因是通配符捕获;基本上每个表达式都先通过通配符捕获,然后再评估上下文表达式。在方法调用中

    foo( arg )

arg 总是在方法 applicability/overloading/inference 完成之前首先捕获通配符。在您的情况下,更通用的类型 Iterable<? extends Thing> 丢失,变为 Iterable<CAP#>.

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


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

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

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

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

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

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

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

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

List<Thing> things = ...;

ThingsProvider thingsProvider = ()->things;