4. 模拟网络响应:MockWebServer 启动、enqueue 响应、设置 HTTP 状态码与 Body

4.1 原理概述

在 Worker 对 Retrofit 网络调用进行单元测试时,真实 HTTP 服务会引入不确定性、环境依赖和速度瓶颈。MockWebServer 是 OkHttp 官方提供的轻量级内嵌 HTTP 服务,允许在本地回放预定义的响应,从而对 Retrofit 的请求/响应生命周期进行隔离验证。

  • 启动:MockWebServer 在测试进程内绑定随机可用端口,无需外部服务。
  • enqueue:将预构建的 MockResponse 对象放入响应队列,服务器按 FIFO 顺序逐条返回。
  • 状态码与 Body:每个 MockResponse 可独立设置 HTTP 状态码、Header 和响应体,覆盖成功、失败、重定向、超时等场景。

4.2 实现流程

  1. 在测试依赖中引入 MockWebServer(com.squareup.okhttp3:mockwebserver)。
  2. 在 @Before 中实例化 MockWebServer 并调用 start()。
  3. 构造 MockResponse 对象,设置状态码、Header 和 Body。
  4. 调用 server.enqueue(mockResponse) 将响应加入队列。
  5. 用 server.url("/api/endpoint") 获取完整 base URL 构建 Retrofit 实例。
  6. 执行 Worker 的网络操作,断言实际结果与预期一致。
  7. 在 @After 中调用 server.shutdown()。
💡 提示:MockWebServer 默认每次调用只消耗一个 enqueue 的响应;若队列为空则阻塞等待下一个响应。可在测试中连续 enqueue 多个响应以模拟分页或重试。

4.3 核心代码示例

以下 Kotlin 测试展示了完整的 MockWebServer 设置与响应模拟:

class WorkerNetworkTest {

    private lateinit var mockWebServer: MockWebServer
    private lateinit var apiService: ApiService

    @Before
    fun setUp() {
        mockWebServer = MockWebServer()
        mockWebServer.start()

        val retrofit = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))  // 使用 MockWebServer 的 base URL
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        apiService = retrofit.create(ApiService::class.java)
    }

    @Test
    fun `test successful response with body`() {
        // 构造 MockResponse
        val successResponse = MockResponse()
            .setResponseCode(200)
            .setHeader("Content-Type", "application/json; charset=utf-8")
            .setBody("""{"userId":1,"title":"test","completed":false}""")

        mockWebServer.enqueue(successResponse)

        val worker = MyNetworkWorker(apiService)
        val result = worker.doWork()

        assertTrue(result.isSuccess)
        assertEquals("test", result.getOrNull()?.title)
    }

    @Test
    fun `test 404 error handling`() {
        val errorResponse = MockResponse()
            .setResponseCode(404)
            .setBody("Not Found")
        mockWebServer.enqueue(errorResponse)

        val worker = MyNetworkWorker(apiService)
        val result = worker.doWork()

        assertTrue(result.isFailure)
    }

    @After
    fun tearDown() {
        mockWebServer.shutdown()
    }
}

4.4 API / 配置详解

方法 说明 典型值
MockWebServer.start() 启动服务器,绑定随机端口 —
MockWebServer.enqueue(MockResponse) 将响应加入 FIFO 队列 —
MockResponse.setResponseCode(int) 设置 HTTP 状态码 200, 400, 404, 500, 503
MockResponse.setHeader(String, String) 添加响应头 Content-Type: application/json
MockResponse.setBody(String) 设置响应体(字符串形式) JSON、XML 或纯文本
MockWebServer.url(String) 返回完整 URL,包含主机和端口 http://127.0.0.1:PORT/api
MockWebServer.shutdown() 优雅关闭服务器,释放端口 —

4.5 调试技巧

  • 使用 server.takeRequest() 获取实际收到的请求对象,调试 Headers、Path 和 Body。
  • 利用 server.requestCount 验证请求次数是否符合预期。
  • 使用 mockWebServer.setDispatcher(Dispatcher) 自定义响应逻辑,实现条件分支。
// 调试示例:打印服务器收到的请求
val request = mockWebServer.takeRequest()
println("Path: ${request.path}")
println("Headers: ${request.headers}")
println("Body: ${request.body.readUtf8()}")
⚠ 常见错误
  • 忘记 start() 导致 BindException。
  • 多次调用 start() 后未 shutdown() 引发端口泄露。
  • enqueue 响应数量少于实际请求次数,导致测试线程被永久阻塞。
  • Base URL 中使用 /api 但未以斜杠结尾,导致 Retrofit 拼接路径异常。

4.6 性能与稳定性优化

  • 尽量在 @BeforeClass 中单次启动 MockWebServer,使用 beforeAll 减少反复启动开销。
  • 为每个测试类使用唯一实例,避免端口冲突(随机端口自动处理)。
  • 对于极低延迟场景,可设置 mockWebServer.setBodyDelay(0) 消除模拟延迟。
📌 知识结构总结
MockWebServer 启动 → 构造 MockResponse(状态码 + Body + Headers)→ enqueue 进队列 → Worker 通过 Retrofit 发出请求 → 服务器按顺序返回 → 断言结果 → 关闭服务器。
MockWebServer 启动与响应模拟流程 1. 启动 MockWebServer 2. 构造 MockResponse 3. enqueue 到队列 4. 执行网络请求 5. 获取响应 & 断言 说明 每条响应支持独立设置 状态码、Header、Body 队列按 FIFO 顺序返回 支持延迟模拟、断连等 TDD 中隔离验证 Retrofit 请求/响应

4.7 常见问题与排查

  • 响应队列空导致测试挂起:检查 enqueue 次数是否足够,或使用 server.setDispatcher 兜底。
  • 端口已被占用:确保每个测试类只启一个实例,且 shutdown 在 finally 块执行。
  • 请求路径不匹配:用 server.takeRequest().path 比对实际路径。
  • Body 编码问题:使用 setBody(ByteString) 或显式设置 charset。
✅ 最佳实践
为每个测试方法(或每组相关方法)enqueue 独立响应集合,并在断言前使用 await().untilAsserted 确保异步 Worker 完成。搭配 IdlingResource 可进一步稳定测试。