如何模拟 riak java 客户端?

How to mock riak java client?

我正在尝试对使用 com.basho.riak:riak-client:2.0.0 的代码进行单元测试。我嘲笑了所有 riak 客户端 classes,并希望得到一个无用但有效的测试。但是,这会因空指针而失败:

java.lang.NullPointerException
  at com.basho.riak.client.api.commands.kv.KvResponseBase.convertValues(KvResponseBase.java:243)
  at com.basho.riak.client.api.commands.kv.KvResponseBase.getValue(KvResponseBase.java:150)
  at com.basho.riak.client.api.commands.kv.FetchValue$Response.getValue(FetchValue.java:171)

我的测试是这样的:

    @Test public void test() {         
        RiakClient riakClient = mock(RiakClient.class);

        @SuppressWarnings("unchecked")
        RiakCommand<FetchValue.Response, Location> riakCommand = (RiakCommand<FetchValue.Response, Location>) mock(RiakCommand.class);

        Response response = mock(Response.class);
        when(riakClient.execute(riakCommand)).thenReturn(response);
        Response returnedResponse = riakClient.execute(riakCommand);

        when(response.getValue(Object.class)).thenReturn(new Object());
        MyPojo myData = returnedResponse.getValue(MyPojo.class);
        // Make assertions
    }

如何对使用 riak 客户端的代码进行单元测试?最后,我想确保使用预期的 type/bucket/key 组合,并且预期的 RiakCommand 是 运行.

编辑:我深入研究了 FetchValue class 并发现了这个结构:
FetchValue - 是 public final

FetchValue.Response
- 是 public static,
- 有一个包私有的构造函数 Response(Init<?> builder)

FetchValue.Response.Init<T> 是:
- protected static abstract class Init<T extends Init<T>> extends KvResponseBase.Init<T>

还有 FetchValue.Response.Builder:
static class Builder extends Init<Builder>
- 使用 build() 时:return new Response(this);

我假设 Mockito 在内部 classes 中的某个地方迷路了,我的调用在 KvResponseBase.convertValues 中结束,NP 被抛出。 KvResponseBase.convertValues 假设有一个 List<RiakObject> 的值,我看不出合理的分配方式。

你不能用 mockito 模拟 final classes 和 final and/or static 方法。请注意,static 嵌套 class 是可以的。这是因为 mockito subclasses(我不是 100% 确定这是确切的操作,它使用 CGLIB 生成 classes)对象,但不允许重写最终方法或扩展最后的 classes。对于 static 方法,永远不可能覆盖。

在您的代码中,您可能正在尝试调用最终的 class 或方法。很难判断哪个 class 导致了问题,从您的 NullPointer stackstrace 您应该怀疑您模拟的第一个对象(从测试用例方法开始)。 mock 上的方法不应该调用任何其他方法(期望 mockito 的内部方法),所以这可能是最终的,因为你似乎没有调用 'mocked' 方法。

在你的情况下,堆栈跟踪不完整(因为你的测试用例不在上面)。在快速浏览 riak 框架时,我找不到该方法,请查看 FetchValue$Response.getValue.

另请注意以下事项。从您发布的代码片段中,我无法判断您在测试用例中测试了什么。您创建的所有对象都是模拟对象。通常你有 1 个(或几个)真实的 classes 正在测试。其他 classes(与你的 classes 进行交互)你模拟,以便能够模拟复杂的行为。

我已经调查了一下你的情况。我已将您的示例简化为这个简单的 SSCCE:

import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import org.junit.Test;
import com.basho.riak.client.api.commands.kv.FetchValue.Response;

public class RiakTest {
    @Test
    public void test() throws Exception {
        Response response = mock(Response.class);
        given(response.getValue(Object.class)).willReturn(new Object());
    }
}

抛出此错误:

java.lang.NullPointerException
at com.basho.riak.client.api.commands.kv.KvResponseBase.convertValues(KvResponseBase.java:243)
at com.basho.riak.client.api.commands.kv.KvResponseBase.getValue(KvResponseBase.java:150)
at com.basho.riak.client.api.commands.kv.FetchValue$Response.getValue(FetchValue.java:171)
at RiakTest.test(RiakTest.java:12)

经过一番挖掘,我想我已经确定了问题所在。这是因为您 试图存根从包(可见性)继承的 public 方法 class:

abstract class KvResponseBase {
    public <T> T getValue(Class<T> clazz) {
    }
}

似乎 Mockito 无法存根此方法,因此调用了真正的方法并抛出 NullPointerException(由于空成员的访问:values)。 需要注意的一件重要事情是,如果此函数调用没有失败,Mockito 将显示正确的错误:

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
    when() requires an argument which has to be 'a method call on a mock'.
    For example:
        when(mock.getArticles()).thenReturn(articles);

    Also, this error might show up because:
    1. you stub either of: final/private/equals()/hashCode() methods.
       Those methods *cannot* be stubbed/verified.
       Mocking methods declared on non-public parent classes is not supported.
    2. inside when() you don't call method on mock but on some other object.

我猜这是一个 Mockito 错误或限制,所以我在 Mockito tracker 中打开了一个问题,我用简单的 classes 重现了你的案例。


更新

The issue i opened is in fact a duplicate of an existing one. This issue will not be fixed but a workaround exists. You may use the Bytebuddy mockmaker instead of the cglib one. Explanations 可以在这里找到。

跟进: 谢谢@gontard,我找到了这个:

<dependency>
  <!-- We need this fix: https://github.com/mockito/mockito/pull/171 to use mockito with Riak -->
<!--
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>2.0.52-beta</version>
  <scope>test</scope>
  </dependency>

其中包含修复程序。

不幸的是,如果您同时使用 Fetch 和 MultiFetch(很可能),那您就麻烦了。

MultiFetch.Response 是一个最终的 class (所以你可以使用 mockito,你需要使用 PowerMock) FetchValue.Response 存在您概述的问题,只能使用 beta mockito 修复,powermock 尚不可用...

更新,我想出了如何同时使用 mockito 和 powermock(直到 powermock 升级):

<!-- We need this to mock Multi-Fetch responses from Riak, which are final -->
<!-- However, we need the beta version of mockito due to bugs (see below),
so we _cannot_ use the mockito api provided by powermock, do _not_ include _powermock-api-mockito, it'll mess stuff up -->
<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-module-junit4</artifactId>
  <version>1.6.4</version>
  <scope>test</scope>
</dependency>
<!--If we don't include this, we get: -->
<!--java.lang.IllegalStateException:
Extension API internal error: org.powermock.api.extension.proxyframework.ProxyFrameworkImpl could not be located in classpath.-->
<!-- it looks like this is due to some discrepancy in packaging with mockito 2, this may be fixed in Fall 2016:
https://groups.google.com/forum/#!topic/powermock/cE4T40Xa_wc -->
<dependency>
  <groupId>org.powermock</groupId>
  <artifactId>powermock-api-easymock</artifactId>
  <version>1.6.4</version>
</dependency>


<!-- We need this fix: https://github.com/mockito/mockito/pull/171 to use mockito with Riak -->
<!--
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>2.0.52-beta</version>
  <scope>test</scope>
  </dependency>