You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mockk库中HttpClient调用验证失败问题排查

Mock HttpClient调用未被检测到的原因分析

错误信息

java.lang.AssertionError: Verification failed: call 2 of 2: BlockingHttpClient(child of #1#3).retrieve(eq(POST /employee/create))) was not called

业务代码

@KafkaListener(offsetReset = OffsetReset.EARLIEST, groupId = "test")
class EmployeeListener(@Client("\${demo.restapi}") val client: HttpClient) {
    var log = KotlinLogging.logger {}
    val gson = GsonBuilder().setPrettyPrinting().create();

    @Topic("employee")
    fun recieveEmployee(str: String) {
        log.info { "Received employee creation request for $str" }

        try {
            val employeeDto  = gson.fromJson(str, EmployeeDto::class.java)

            val request: HttpRequest<Any> = HttpRequest.POST("/employee/create", employeeDto)
            val body = client.toBlocking().retrieve(request)
        } catch (e: JsonSyntaxException) {
            log.error ("Error parsing json", e)
        }

        log.info("Employee creation request sent successfully")
    }
}

初始测试代码

val httpClient = mockk<HttpClient>(relaxed = true)
val blockingHttpClient = mockk<BlockingHttpClient>(relaxed = true)

test("Employee listener test") {
    val listener = EmployeeListener(httpClient)

    val request: HttpRequest<Any> = HttpRequest.POST("/employee/create", EmployeeDto("Test1"))

    every { httpClient.toBlocking() } returns blockingHttpClient
    every { blockingHttpClient.retrieve(any()) } returns "{'name': 'Test1'}"

    listener.recieveEmployee("{'name': 'Test1'}")

    verify { httpClient.toBlocking().retrieve(request) }
}

更新后的测试代码(仍失效)

@MicronautTest
class EmployeeListenerTest: FunSpec( {

    val httpClient = mockk<HttpClient>(relaxed = true)
    val blockingHttpClient = mockk<BlockingHttpClient>(relaxed = true)

    test("Employee listener test") {
        val listener = EmployeeListener(httpClient)

        val request: HttpRequest<Any> = HttpRequest.POST("/employee/create", EmployeeDto("Test1"))

        every { httpClient.toBlocking().retrieve(any()) } returns "{'name': 'Test1'}"
        //every { blockingHttpClient.retrieve(any()) } returns "{'name': 'Test1'}"

        listener.recieveEmployee("{'name': 'Test1'}")

        verify { httpClient.toBlocking().retrieve(any()) }
    }

})

问题原因

  1. 对象实例不匹配:测试代码中手动创建的request对象,和EmployeeListener内部生成的request是完全独立的两个实例。默认情况下,HttpRequest的equals方法不会认为这两个对象相等,导致verify时用eq(request)匹配失败。

  2. Mock链式调用的处理问题:

    • 初始测试中,虽然已经mock了httpClient.toBlocking()返回blockingHttpClient,但verify { httpClient.toBlocking().retrieve(request) }这种链式调用的验证方式,Mockk无法正确关联到之前mock的blockingHttpClient实例,导致验证失效。
    • 更新后的测试直接mockhttpClient.toBlocking().retrieve(any()),这种写法不符合Mockk对多层mock的处理逻辑,无法正确绑定到实际的调用链路。

解决方案

方案1:修改验证逻辑,使用模糊匹配或属性匹配

放弃直接匹配request实例,改用any()匹配任意HttpRequest,或者通过属性校验确保请求符合预期:

// 方式1:匹配任意HttpRequest
verify { blockingHttpClient.retrieve(any<HttpRequest<Any>>()) }

// 方式2:校验请求的关键属性
verify {
    blockingHttpClient.retrieve(
        match { req ->
            req.method == HttpMethod.POST &&
            req.path == "/employee/create" &&
            (req.body as EmployeeDto).name == "Test1"
        }
    )
}

方案2:确保Mock链路的一致性

严格按照调用链路分层mock,并直接验证底层mock对象的方法调用:

test("Employee listener test") {
    val listener = EmployeeListener(httpClient)

    // 分层mock:先mock toBlocking()返回指定的blockingHttpClient
    every { httpClient.toBlocking() } returns blockingHttpClient
    // mock retrieve方法的返回值
    every { blockingHttpClient.retrieve(any()) } returns "{'name': 'Test1'}"

    listener.recieveEmployee("{'name': 'Test1'}")

    // 直接验证blockingHttpClient的retrieve方法被调用
    verify { blockingHttpClient.retrieve(any()) }
}

方案3:重写equals方法(可选)

如果需要严格匹配EmployeeDto和HttpRequest,可以重写EmployeeDto的equals和hashCode方法,确保内容相同的对象能被判定为相等,这样测试中手动创建的request就能和Listener内部生成的request匹配成功。

内容的提问来源于stack exchange,提问作者Bharat Jain

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 18:51:15