【问题标题】:Hit with Unimplemented method StatusRuntimeException when unit testing generated grpc code in Android project在 Android 项目中对生成的 grpc 代码进行单元测试时,使用未实现的方法 StatusRuntimeException 命中
【发布时间】:2023-03-17 21:50:02
【问题描述】:

我有一个 Android 项目,它在构建时从 proto 文件生成 gRPC 类,它来自 helloworld 示例下的此处: https://github.com/grpc/grpc-java/tree/master/examples/src

我的 JUnit 测试遵循为 HelloWorldClient (https://github.com/grpc/grpc-java/blob/master/examples/example-kotlin/src/test/kotlin/io/grpc/examples/helloworld/HelloWorldClientTest.kt) 编写的测试,我认为理论上可行

但是,当我运行测试时,我遇到了如下异常错误:

io.grpc.StatusRuntimeException: UNIMPLEMENTED: Method helloworld.Greeter/SayHello is unimplemented
    at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:235)
    at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:216)
    at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:141)
    at io.grpc.examples.helloworld.GreeterGrpc$GreeterBlockingStub.sayHello(GreeterGrpc.java:177)
    at com.android.grpcmvvm.data.GreeterRemoteDataSource.sayHello(GreeterRemoteDataSource.kt:49)
    at com.android.grpcmvvm.view.GreeterViewModelUnitTest.testTestFunction(GreeterViewModelUnitTest.kt:109)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at io.grpc.testing.GrpcCleanupRule$1.evaluate(GrpcCleanupRule.java:125)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)


Wanted but not invoked:
serviceImpl.sayHello(
    <Capturing argument>,
    <any>
);
-> at io.grpc.examples.helloworld.GreeterGrpc$GreeterImplBase.sayHello(GreeterGrpc.java:101)

However, there was exactly 1 interaction with this mock:
serviceImpl.bindService();
-> at io.grpc.internal.AbstractServerImplBuilder.addService(AbstractServerImplBuilder.java:114)


Wanted but not invoked:
serviceImpl.sayHello(
    <Capturing argument>,
    <any>
);
-> at io.grpc.examples.helloworld.GreeterGrpc$GreeterImplBase.sayHello(GreeterGrpc.java:101)

However, there was exactly 1 interaction with this mock:
serviceImpl.bindService();
-> at io.grpc.internal.AbstractServerImplBuilder.addService(AbstractServerImplBuilder.java:114)


    at io.grpc.examples.helloworld.GreeterGrpc$GreeterImplBase.sayHello(GreeterGrpc.java:101)
    at com.android.grpcmvvm.view.GreeterViewModelUnitTest.testTestFunction(GreeterViewModelUnitTest.kt:112)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at io.grpc.testing.GrpcCleanupRule$1.evaluate(GrpcCleanupRule.java:125)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

以下是测试和相关代码 (请注意 ViewModel 的名称。出于此问题的目的,我从测试中删除了有关 ViewModel 的任何内容)

GreeterViewModelUnitTest.kt

import com.android.grpcmvvm.data.GreeterRemoteDataSource
import com.android.grpcmvvm.grpc.GrpcService
import io.grpc.ManagedChannel
import io.grpc.examples.helloworld.GreeterGrpc
import io.grpc.examples.helloworld.HelloReply
import io.grpc.examples.helloworld.HelloRequest
import io.grpc.inprocess.InProcessChannelBuilder
import io.grpc.inprocess.InProcessServerBuilder
import io.grpc.stub.StreamObserver
import io.grpc.testing.GrpcCleanupRule
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.*
import org.mockito.AdditionalAnswers.delegatesTo
import org.mockito.internal.matchers.Any

@RunWith(JUnit4::class)
class GreeterViewModelUnitTest {
    // region JUnit test rules
    @get:Rule
    val grpcCleanupRule = GrpcCleanupRule()

    private val serviceImpl = Mockito.mock(GreeterGrpc.GreeterImplBase::class.java, delegatesTo<Any>(object: GreeterGrpc.GreeterImplBase(){}))
    // endregion

    // region Private properties
    @Mock
    private lateinit var grpcService: GrpcService

    private lateinit var managedChannel: ManagedChannel

    private lateinit var greeterRemoteDataSource: GreeterRemoteDataSource
    // endregion

    @Before
    @Throws(Exception::class)
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        val serverName = InProcessServerBuilder.generateName()

        grpcCleanupRule.register(InProcessServerBuilder
            .forName(serverName)
            .directExecutor()
            .addService(serviceImpl)
            .build()
            .start())

        managedChannel = grpcCleanupRule.register(InProcessChannelBuilder
            .forName(serverName)
            .directExecutor()
            .build())

        Mockito.`when`(grpcService.createManagedChannel()).thenReturn(managedChannel)
        greeterRemoteDataSource = GreeterRemoteDataSource(grpcService)
    }

    @Test
    fun testTestFunction() {
        val requestCaptor = ArgumentCaptor.forClass(HelloRequest::class.java)

        greeterRemoteDataSource.sayHello("once again")

        Mockito.verify<GreeterGrpc.GreeterImplBase>(serviceImpl)
            .sayHello(requestCaptor.capture(), ArgumentMatchers.any<StreamObserver<HelloReply>>())

        assertEquals("once again", requestCaptor.value.name)
    }

GrpcService.kt

import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import java.util.concurrent.Executors

class GrpcService(private val host: String, private val port: Int) {

    fun createManagedChannel(): ManagedChannel {
        return ManagedChannelBuilder
            .forAddress(host, port)
            .executor(Executors.newSingleThreadExecutor())
            .usePlaintext()
            .build()
    }
}

GreeterRemoteDataSource.kt

import com.android.grpcmvvm.grpc.GrpcService
import io.grpc.ManagedChannel
import io.grpc.StatusRuntimeException
import io.grpc.examples.helloworld.GreeterGrpc
import io.grpc.examples.helloworld.HelloReply
import io.grpc.examples.helloworld.HelloRequest

class GreeterRemoteDataSource constructor(private val grpcService: GrpcService) {

    private lateinit var channel: ManagedChannel

    fun sayHello(message: String): String {
        channel = grpcService.createManagedChannel()

        val stub = GreeterGrpc.newBlockingStub(channel)
        val request = HelloRequest.newBuilder().setName(message).build()

        val reply: HelloReply = try {
            stub.sayHello(request)
        } catch (e: StatusRuntimeException) {
            e.printStackTrace()
            return String.format("{0}", e.status)
        } finally {
            channel.shutdown()
        }

        return reply.message
    }
}

build.gradle(应用程序)

apply plugin: 'com.android.application'

apply plugin: 'com.google.protobuf'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.android.grpcmvvm"
        minSdkVersion 15
        targetSdkVersion 28
        multiDexEnabled true
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    def grpc_version = "1.22.1"
    def lifecycle_version = "2.2.0-alpha04"
    def mockito_kotlin_version = "2.1.0"
    def mockito_version = "3.0.0"
    def multidex_version = "2.0.1"
    def truth_version = "0.45"

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation "androidx.multidex:multidex:$multidex_version"
    implementation 'com.google.android.material:material:1.0.0'

    // gRPC
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation "io.grpc:grpc-okhttp:$grpc_version"
    implementation "io.grpc:grpc-protobuf-lite:$grpc_version"
    implementation "io.grpc:grpc-stub:$grpc_version"

    // Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

    // Instrumented tests
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    androidTestImplementation 'androidx.test:runner:1.2.0'

    // Unit tests
    testImplementation "androidx.arch.core:core-testing:2.1.0"
    testImplementation "com.google.truth:truth:$truth_version"
    testImplementation "io.grpc:grpc-testing:$grpc_version"
    testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin_version"
    testImplementation 'junit:junit:4.12'
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.1'
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.1'
    testImplementation "org.mockito:mockito-inline:$mockito_version"
}

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.9.0'
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:1.22.1"
        }
        javalite {
            artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            javalite {}
        }
        ofNonTest()*.plugins {
            grpc {
                // Options added to --grpc_out
                option 'lite'
            }
        }
    }
}

build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        google()
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()

    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

我目前无法找到导致此类错误的确切原因。我也四处寻找答案/见解,但无济于事。

感谢任何输入。

【问题讨论】:

    标签: android unit-testing kotlin mockito grpc


    【解决方案1】:

    您正在使用mockito-inline,它正在模拟final 方法。如果您换成mockito-core,那么测试应该会通过。我用 example-kotlin 对此进行了测试;将 mockito 依赖项交换为 mockito-inline 破坏了测试。我会提到 grpc-java 强烈不支持覆盖 final 方法。

    作为一种快速修复,您可以使用:

    Mockito.`when`(grpcService.bindService()).thenCallRealMethod()
    

    【讨论】:

      【解决方案2】:

      sayHello没有实现,所以异常正确:

          private val serviceImpl = Mockito.mock(GreeterGrpc.GreeterImplBase::class.java, delegatesTo<Any>(object: GreeterGrpc.GreeterImplBase(){}))
      

      示例没有失败,因为 HelloWorldClient 捕获了异常并记录了它。如果您查看 build/reports/tests/test/index.html 并导航到 HelloWorldClientTest,标准错误包括:

      Sep 18, 2019 9:18:11 AM io.grpc.examples.helloworld.HelloWorldClient greet
      WARNING: RPC failed: Status{code=UNIMPLEMENTED, description=Method helloworld.Greeter/SayHello is unimplemented, cause=null}
      

      要解决这个问题,只需提供 sayHello 方法的假实现:

          private val serviceImpl = Mockito.mock(GreeterGrpc.GreeterImplBase::class.java, delegatesTo<Any>(object: GreeterGrpc.GreeterImplBase(){
              override fun sayHello(req: HelloRequest, responseObserver: StreamObserver<HelloReply>) {
                  responseObserver.onNext(HelloReply.getDefaultInstance())
                  responseObserver.onCompleted()
              }
          }))
      

      我已提交issue 6161 以使示例更清晰。

      【讨论】:

      • 我误解了你所说的失败。这都是真的,但不是测试失败的核心问题。
      • 谢谢!老实说,我不敢相信我错过了那部分。至于模拟,就像你建议的那样,我已经更改为 mockito-core 并使用 Kotlin 的全开放插件来打开我打算在测试中模拟的 Kotlin 类。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-02-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多