4. MockWebServer入门:添加依赖、创建MockServer、启动与关闭

搞测试最烦什么?就是依赖外部环境。网络请求一跑就崩,服务器挂了你就卡在那。我这些年干的最爽的一件事,就是学会用MockWebServer——一个轻量级的HTTP服务器,专门用来在测试里模拟后端。

说白了,你的App发请求,MockWebServer替你挡在前面。你要什么响应,就给它配什么响应。简单、可控、而且跑得飞快。

MockWebServer 在测试中的角色 测试用例 Retrofit 客户端 MockWebServer 创建并注入端口 HTTP 请求 预配置的 Mock 响应 解析后的数据 / 错误 1. 创建实例 2. 启动(start) 3. enqueue 响应 4. 执行测试 MockWebServer 完全在本地运行,无网络开销,毫秒级启动

4.1 添加依赖

你用的项目如果是Kotlin + Gradle,那加一行就够了。记住,这是test依赖,别搞到主工程里去。

dependencies {
    // 其他依赖...
    testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
}

版本号我推荐用最新的4.x稳定版。别问我为什么不用3.x,我在项目里踩过坑——3.x的某些API在老版本OkHttp上会报ClassNotFoundException。你如果还在用OkHttp 3.x,记得看兼容表。

MockWebServer 版本OkHttp 兼容版本备注
4.12.0OkHttp 4.x (Kotlin)推荐
3.14.9OkHttp 3.xJava版本,API略有差异

如果你用Maven或Gradle Kotlin DSL,语法稍有不同。但关键点一样:testCompile 'com.squareup.okhttp3:mockwebserver:4.12.0'

4.2 创建 MockServer 实例

创建起来超级简单。就是一个new操作,或者用Kotlin的构造函数。

val mockServer = MockWebServer()

等你拿到这个实例,它就是你的“私有服务器”了。端口什么的还不用管,后面启动时会自动分配。我个人习惯把这个实例声明在测试类的顶层,然后用@Before初始化。

class ApiServiceTest {
    private lateinit var mockServer: MockWebServer
    
    @Before
    fun setUp() {
        mockServer = MockWebServer()
        mockServer.start()
    }
    
    // ... 测试用例
}

注意:每个测试方法之间最好重新创建MockWebServer实例,避免前一个测试的响应队列污染下一个测试。我见过团队为了省事只关不建,结果测试跑得乱七八糟的。

4.3 启动 MockServer

启动方法就一个:start()。它会自动选择一个可用端口(通常从1080开始往上找)。你也可以指定端口:start(port)

mockServer.start()                   // 随机端口
mockServer.start(9090)               // 固定端口

我建议大多数时候用随机端口,因为测试环境不会有端口冲突。有一次我们在CI上并行跑测试,有人固定了端口,结果好几个job全挂了——端口被占。

启动后,你需要的其实是它的端口号,用来配置你的Retrofit Base URL:

val baseUrl = "http://localhost:${mockServer.port}/"
val retrofit = Retrofit.Builder()
    .baseUrl(baseUrl)
    .client(OkHttpClient.Builder().build())
    .build()

核心记忆点:start() 启动服务器,mockServer.port 获取端口,enqueue() 预设响应。

4.4 关闭 MockServer

用完记得关,不然下一个测试跑的时候端口被占,或者进程残留。关闭方法:

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

shutdown()会优雅地关闭:停止接收新连接,等待正在处理的请求完成(可以设超时),然后释放端口。你如果只是简单断开,直接调close()也行,但我建议用shutdown()保平安。

Kotlin用户可以考虑use函数:MockWebServer().use { server -> ... },但要注意use会自动调用close,和shutdown的区别在于,close不会等待请求处理。我一般还是手写@After更放心。

4.5 完整的开胃示例

咱们别光说不练。搞个最简单的测试,看看MockWebServer怎么运作。

class SimpleMockTest {
    private lateinit var mockServer: MockWebServer
    
    @Before
    fun setUp() {
        mockServer = MockWebServer()
        mockServer.start()
    }
    
    @After
    fun tearDown() {
        mockServer.shutdown()
    }
    
    @Test
    fun `mock server 返回200`() {
        // 预设一个响应
        mockServer.enqueue(MockResponse()
            .setResponseCode(200)
            .setBody("{\"message\":\"hello\"}")
            .addHeader("Content-Type", "application/json"))
        
        // 创建OkHttp客户端直接发请求(演示用)
        val client = OkHttpClient()
        val request = Request.Builder()
            .url("http://localhost:${mockServer.port}/test")
            .build()
        
        val response = client.newCall(request).execute()
        
        assertEquals(200, response.code())
        assertTrue(response.body()?.string()?.contains("hello") ?: false)
        
        // 可选:检查MockServer收到的请求
        val recordedRequest = mockServer.takeRequest()
        assertEquals("/test", recordedRequest.path)
    }
}

这个例子跑完,你就理解了:start() -> enqueue() -> 发请求 -> 断言 -> shutdown()。

4.6 常见避坑

  • 端口占用:我遇到过两次。一次是忘记shutdown,一次是CI上两个测试类用了同一个固定端口。解决方案:用随机端口,或者用@Before里重新new实例。
  • 响应队列用完:如果你enqueue了3个响应,但发了4个请求,第4个会收到404。所以要么数对着,要么用mockServer.enqueue()循环加个默认的兜底。
  • 多线程请求:MockWebServer是单线程处理的,如果你在测试里发了多个并发请求,注意顺序可能乱。可以用mockServer.takeRequest(timeout, unit)配合超时处理。

我曾经在重构一个老项目时,发现某测试用例的@After里只写了mockServer.close()而不是shutdown(),结果导致后续测试端口绑定失败。排查了半天。所以记住:shutdown() > close()。

4.7 小结

好了,MockWebServer的基本操作就这些。添加依赖 → 创建实例 → start() → enqueue() → shutdown()。你会发现它比你想象中简单得多——其实就是个嵌入式HTTP服务器,只不过专门给测试用的。下一关咱们就要用它和Retrofit真正搭伙了。