【问题标题】:How to use mockito to mock grpc ServiceBlockingStub to throw StatusRuntimeException(Status.UNAVAILABLE)?如何使用mockito来模拟grpc ServiceBlockingStub抛出StatusRuntimeException(Status.UNAVAILABLE)?
【发布时间】:2020-04-19 13:04:00
【问题描述】:

我想模拟我的 grpc 客户端,以通过抛出 new StatusRuntimeException(Status.UNAVAILABLE) 来确保它能够抵御故障(这是在将 java.net.ConnectException: Connection refused 抛出到 grpc 客户端时抛出的异常)。但是,生成的类是 final 的,所以 mock 不起作用。

如何让 BlahServiceBlockingStub 抛出 new StatusRuntimeException(Status.UNAVAILABLE) 而无需重构我的代码以围绕 BlahServiceBlockingStub 创建一个包装类?

这是我尝试过的(BlahServiceBlockingStub 是由 grpc 生成的):

    @Test
    public void test() {
        BlahServiceBlockingStub blahServiceBlockingStub = mock(BlahServiceBlockingStub.class);

        when(blahServiceBlockingStub.blah(any())).thenThrow(new StatusRuntimeException(Status.UNAVAILABLE));


        blahServiceBlockingStub.blah(null);
    }

不幸的是,我得到了预期的以下异常:

org.mockito.exceptions.base.MockitoException: 
Cannot mock/spy class BlahServiceGrpc$BlahServiceBlockingStub
Mockito cannot mock/spy following:
  - final classes
  - anonymous classes
  - primitive types

    at MyTestClass.test(MyTestClass.java:655)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
.
.
.

因为我尝试模拟 grpc 生成的最终类:

  public static final class BlahServiceBlockingStub extends io.grpc.stub.AbstractStub<BlahServiceBlockingStub> {
    private BlahServiceBlockingStub(io.grpc.Channel channel) {
      super(channel);
    }

【问题讨论】:

    标签: java mockito grpc


    【解决方案1】:

    您有几个选择:

    请注意为什么在这种情况下模拟 final 可能是个坏主意: 根据具体情况,模拟 final 类或方法可能不是一个好主意。细节决定成败。在您的情况下,您正在创建生成代码的模拟,因此您假设生成的代码将来会如何表现。 gRPC 和 Protobuf 仍在快速发展,因此做出这些假设可能会有风险,因为它们可能会发生变化,而您不会注意到,因为您不会根据生成的代码检查模拟。因此,除非您真的必须这样做,否则模拟生成的代码并不是一个好主意。

    【讨论】:

      【解决方案2】:

      不要模拟客户端存根或任何其他最终类/方法。 gRPC 团队可能会不遗余力地破坏您对此类模拟的使用,因为它们非常脆弱并且会产生“不可能”的结果。

      模拟服务,而不是客户端存根。当与过程中传输相结合时,它会产生快速、可靠的测试。这与grpc-java hello world example 中演示的方法相同。

      @Rule
      public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
      
      @Test
      public void test() {
          // This can be a mock, but is easier here as a fake implementation
          BlahServiceImplBase serviceImpl = new BlahServiceImplBase() {
              @Override public void blah(Request req, StreamObserver<Response> resp) {
                  resp.onError(new StatusRuntimeException(Status.UNAVAILABLE));
              }
          };
          // Note that the channel and server can be created in any order
          grpcCleanup.register(InProcessServerBuilder.forName("mytest")
              .directExecutor().addService(serviceImpl).build().start());
          ManagedChannel chan = grpcCleanup.register(
              InProcessChannelBuilder.forName("mytest").directExecutor().build();
          BlahServiceBlockingStub blahServiceBlockingStub
              = BlahServiceGrpc.newBlockingStub();
      
          blahServiceBlockingStub.blah(null);
      }
      

      在进行多个测试时,您可以将服务器、通道和存根创建提升到字段或 @Before 中,从单独的测试中取出。这样做时,在服务器上使用MutableHandlerRegistry 作为fallbackHandlerRegistry() 会很方便。这允许您在服务器启动后注册服务。有关该方法的更完整示例,请参阅route guide example

      【讨论】:

      • 我现在正在尝试这个。如果我遇到困难,我会参考您提供的路线指南示例。我将在此处的评论中更新我的结果。这更干净,因为我不必在我的解决方法中 spy() 我正在单元测试的类。
      • 我使用您的示例浏览了我们的代码库。我们有一个由 protoc-grpc-moles 编译器插件生成的 BlahMoles 用于单元测试。生成的代码与您的示例具有相同的代码:responseObserver.onError(error.get())。如果没有您的回答,我将无法找到它。
      • 我添加了private BlahServiceMole blahBackend = spy(BlahServiceMole.class);(由protoc-grpc-moles compiler plugin 生成的BlahServiceMole),然后在我的@Test 方法中添加了doReturn(Optional.of(new StatusRuntimeException(Status.UNAVAILABLE))).when(blahBackend).blahError(any());。从那里,鼹鼠在函数 blah(input, responseObserver) 中将 blahError 转换为 onError
      • 这种方法是一种比模拟更冗长且更难阅读的方法。是否有任何库可以帮助一般地提供请求/响应解决方案?
      【解决方案3】:

      我最终得到了一个丑陋的解决方法。

      我在引用 BlahServiceBlockingStub 的类上创建了一个新方法和一个 spy()

      生成的代码最终看起来像:

          @Test
          public void test() {
              MyClass myClass = spy(myClass);
      
              doThrow(new StatusRuntimeException(Status.UNAVAILABLE)).when(myClass).newMethod(any());
      
              // changed to call myClass.newMethod() instead of blahServiceBlockingStub.blah
              myClass.myExistingMethod(); 
          }
      

      【讨论】:

        【解决方案4】:

        如何使用 mockito 模拟最终类/方法:

        添加依赖Mockito Inline

        创建文件src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

        一行:mock-maker-inline

        现在您可以模拟 final 方法和类。

        Mockito docs about mocking

        【讨论】:

        • 是的,看起来它会起作用。但是我的项目卡在 1.10.19 上,我无法更改。我无权迁移到 Mockito 2。
        • 不要模拟 final 方法和类。模拟已经足够脆弱了。模拟最终方法/类会产生“不可能”的结果,这会破坏事情(如果不是今天,那么明天)。 grpc-java 已将许多东西做成 final 以防止它们被嘲笑。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-07-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-04-21
        相关资源
        最近更新 更多