如何在Kotlin中Mock HttpClient与HttpRequest进行单元测试?
如何Mock HttpClient实现Spring Boot方法的单元测试
核心结论
完全可以Mock这些HTTP响应,不过原代码中直接在方法内创建HttpClient的写法会增加Mock难度,推荐先重构代码遵循依赖注入原则,再编写测试。
第一步:重构原代码(推荐方案)
将HttpClient和ObjectMapper作为构造参数注入到FileDetails类中,这样测试时可以轻松传入Mock实例:
class FileDetails( private val httpClient: HttpClient, private val objectMapper: ObjectMapper ) { fun getFileDetails(auth: String, id: String): FileModel { val url = URI.create("http://localhost:8080/$id") val request = HttpRequest.newBuilder() .uri(url) .header("Authorization", auth) .build() val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) return objectMapper.readValue(response.body(), object : TypeReference<FileModel>() {}) } }
第二步:编写单元测试
使用Mockito完成Mock和验证,步骤清晰可复用:
import org.junit.jupiter.api.Test import org.mockito.Mockito.* import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.core.type.TypeReference import org.junit.jupiter.api.Assertions.assertEquals class FileDetailsTest { @Test fun `test get file details returns FileModel when response is 200`() { // 1. 准备测试数据 val testAuth = "Bearer token123" val testId = "file-123" val expectedFile = FileModel(id = testId, name = "test.txt", size = 1024) val mockJson = """{"id":"$testId","name":"test.txt","size":1024}""" // 2. 创建Mock实例 val mockHttpClient = mock(HttpClient::class.java) val mockResponse = mock(HttpResponse::class.java) as HttpResponse<String> val mockObjectMapper = mock(ObjectMapper::class.java) // 3. 配置Mock行为 // 让HttpClient.send返回Mock的响应 `when`(mockHttpClient.send(any(HttpRequest::class.java), eq(HttpResponse.BodyHandlers.ofString()))) .thenReturn(mockResponse) // 让Mock响应返回预设的JSON字符串 `when`(mockResponse.body()).thenReturn(mockJson) // 让ObjectMapper将JSON转换为预期的FileModel `when`(mockObjectMapper.readValue(mockJson, any(TypeReference::class.java))) .thenReturn(expectedFile) // 4. 执行被测方法 val fileDetails = FileDetails(mockHttpClient, mockObjectMapper) val result = fileDetails.getFileDetails(testAuth, testId) // 5. 验证结果与Mock调用 assertEquals(expectedFile, result) verify(mockHttpClient).send(any(HttpRequest::class.java), eq(HttpResponse.BodyHandlers.ofString())) verify(mockResponse).body() verify(mockObjectMapper).readValue(mockJson, any(TypeReference::class.java)) } }
备选方案:不重构代码的静态Mock(不推荐)
如果无法修改原代码,可以用Mockito的静态Mock功能MockHttpClient的静态创建方法,但这种方式容易引入测试污染,维护成本高:
@Test fun `test get file details without refactoring`() { val testAuth = "Bearer token123" val testId = "file-123" val expectedFile = FileModel(id = testId, name = "test.txt", size = 1024) val mockJson = """{"id":"$testId","name":"test.txt","size":1024}""" val mockHttpClient = mock(HttpClient::class.java) val mockResponse = mock(HttpResponse::class.java) as HttpResponse<String> val mockObjectMapper = mock(ObjectMapper::class.java) // 静态Mock HttpClient的创建逻辑 mockStatic(HttpClient::class.java).use { mockedClient -> val mockBuilder = mock(HttpClient.Builder::class.java) mockedClient.`when` { HttpClient.newBuilder() }.thenReturn(mockBuilder) mockedClient.`when` { mockBuilder.build() }.thenReturn(mockHttpClient) // 配置Mock行为 `when`(mockHttpClient.send(any(HttpRequest::class.java), eq(HttpResponse.BodyHandlers.ofString()))) .thenReturn(mockResponse) `when`(mockResponse.body()).thenReturn(mockJson) `when`(mockObjectMapper.readValue(mockJson, any(TypeReference::class.java))) .thenReturn(expectedFile) // 如果原类的objectMapper是私有成员,需要通过反射注入Mock实例 val fileDetails = FileDetails() val field = FileDetails::class.java.getDeclaredField("objectMapper") field.isAccessible = true field.set(fileDetails, mockObjectMapper) // 执行测试 val result = fileDetails.getFileDetails(testAuth, testId) assertEquals(expectedFile, result) } }
内容的提问来源于stack exchange,提问作者Sayantan Ghosh
相关产品推荐
相关产品推荐

