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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 21:15:30