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

SpringBoot 3.2 Mockk测试RestClient:不同请求体被识别为同一请求

如何验证SpringBoot 3.2 RestClient调用的请求体正确性?

我在使用SpringBoot 3.2新引入的RestClient时,业务逻辑会两次调用同一API但传入不同请求体。测试时用Mockk模拟RestClient请求,原本期望验证两个不同请求体的调用各执行一次(之前用RestTemplate时逻辑正常),但测试失败,两个请求被识别为同一请求。

测试代码如下:

@Test
fun `test API call 2 times`() {
  every {
    restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(APIRequest(foo))
            .retrieve()
            .toEntity(String::class.java)
    } returns ResponseEntity(HttpStatus.OK)
  }
  every {
    restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(APIRequest(bar))
            .retrieve()
            .toEntity(String::class.java)
    } returns ResponseEntity(HttpStatus.OK)
  }
  
  service.process()

  verify(exactly = 1) {
    restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(APIRequest(foo))
            .retrieve()
            .toEntity(String::class.java)
  }
  verify(exactly = 1) {
    restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(APIRequest(bar))
            .retrieve()
            .toEntity(String::class.java)
  }
}

测试失败报错:

java.lang.AssertionError: Verification failed: call 1 of 6: RestClient(restClient#2).post()). 2 matching calls found, but needs at least 1 and at most 1 calls
Calls:
1) RestClient(restClient#2).post()
2) RestClient(restClient#2).post()

试过用@RestClientTest,但它只能模拟端点响应,没法验证请求体。请问怎么验证RestClient实际调用时用了正确的请求体?


问题原因

RestClient的调用链是流式API,Mockk在匹配链式调用时,默认会把post()后的所有方法调用视为同一链的一部分,但如果直接按完整链匹配,容易因为对象相等性(比如APIRequest的equals方法未正确实现)或者链式调用的匹配逻辑导致识别错误。

解决方法

1. 确保APIRequest实现正确的equals和hashCode

如果APIRequest是自定义普通类,必须手动实现equals和hashCode;如果是Kotlin数据类,默认已自动生成该逻辑,可跳过此步。只有对象相等性判断正确,Mockk才能准确匹配不同的请求体。

2. 使用参数捕获器(CapturingSlot)验证请求体

通过Mockk的CapturingSlot捕获所有调用的请求体,再断言捕获内容符合预期:

@Test
fun `test API call 2 times`() {
    // 定义列表存储所有请求体捕获槽
    val requestSlots = mutableListOf<CapturingSlot<APIRequest>>()
    
    // 匹配所有post请求,捕获body参数
    every {
        restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(capture(requestSlots))
            .retrieve()
            .toEntity(String::class.java)
    } returns ResponseEntity(HttpStatus.OK)
    
    service.process()
    
    // 验证总调用次数
    verify(exactly = 2) {
        restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(any<APIRequest>())
            .retrieve()
            .toEntity(String::class.java)
    }
    
    // 验证捕获到的请求体内容
    val capturedRequests = requestSlots.map { it.captured }
    assert(capturedRequests.containsAll(listOf(APIRequest(foo), APIRequest(bar))))
    assert(capturedRequests.size == 2)
}

3. 用match函数精确匹配请求体

如果不想依赖对象的equals方法,可以用Mockk的match函数自定义匹配逻辑:

@Test
fun `test API call 2 times`() {
    every {
        restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(match { it.foo == foo })
            .retrieve()
            .toEntity(String::class.java)
    } returns ResponseEntity(HttpStatus.OK)
    
    every {
        restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(match { it.foo == bar })
            .retrieve()
            .toEntity(String::class.java)
    } returns ResponseEntity(HttpStatus.OK)
    
    service.process()
    
    verify(exactly = 1) {
        restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(match { it.foo == foo })
            .retrieve()
            .toEntity(String::class.java)
    }
    
    verify(exactly = 1) {
        restClient
            .post()
            .uri(URL)
            .headers(any())
            .body(match { it.foo == bar })
            .retrieve()
            .toEntity(String::class.java)
    }
}

4. 拆分链式调用的Mock对象

把RestClient的链式调用拆分成多个中间对象的Mock,精准匹配每一步调用:

@Test
fun `test API call 2 times`() {
    // Mock链式调用的各个中间对象
    val requestBodyUriSpec = mockk<RestClient.RequestBodyUriSpec>()
    val requestBodySpec = mockk<RestClient.RequestBodySpec>()
    val responseSpec = mockk<RestClient.ResponseSpec>()
    
    every { restClient.post() } returns requestBodyUriSpec
    every { requestBodyUriSpec.uri(URL) } returns requestBodySpec
    every { requestBodySpec.headers(any()) } returns requestBodySpec
    every { requestBodySpec.retrieve() } returns responseSpec
    every { responseSpec.toEntity(String::class.java) } returns ResponseEntity(HttpStatus.OK)
    
    // 分别匹配两个请求体
    every { requestBodySpec.body(APIRequest(foo)) } returns requestBodySpec
    every { requestBodySpec.body(APIRequest(bar)) } returns requestBodySpec
    
    service.process()
    
    // 验证两个请求体各被调用一次
    verify(exactly = 1) { requestBodySpec.body(APIRequest(foo)) }
    verify(exactly = 1) { requestBodySpec.body(APIRequest(bar)) }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 09:20:34