网络请求的单元测试:为什么需要Mock,传统测试的痛点

大家有没有碰到过这种情况——写完一个网络请求的单元测试,跑一遍,绿了。结果第二天一上班,测试全红。你查了半天,发现是后端某个接口挂了。你写的测试,居然因为服务端挂了而失败。

这其实是个经典问题。我以前也这么想:测试嘛,能真实请求接口才是最靠谱的。但后来被坑了几次,才慢慢明白——单元测试不该依赖外部环境。

这一章,我们就聊聊为什么网络请求测试里,Mock是必需品,以及传统测试方式到底痛在哪里。

传统测试方式:直接请求真实接口

先看一个典型的测试代码。假设我们有个ApiService,用来获取用户信息:

public class UserServiceTest {
    private ApiService apiService;

    @Before
    public void setup() {
        // 直接创建一个真实客户端
        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build();
        apiService = retrofit.create(ApiService.class);
    }

    @Test
    public void testGetUser() throws Exception {
        User user = apiService.getUser(1).execute().body();
        Assert.assertNotNull(user);
        Assert.assertEquals("张三", user.getName());
    }
}

这段代码看着没问题对吧?但实际上,它的问题特别多。我总结了四个核心痛点:

痛点 具体表现 影响
依赖外部环境 测试能否通过,完全取决于API是否可用 网络波动、服务重启、版本变更都可能导致测试失败
测试速度慢 每次测试要发起真实网络请求 几十个接口跑一轮要几分钟,CI流水线被拖慢
难以覆盖边界场景 你无法控制服务端返回超时、500错误、空数据 异常处理和边界逻辑几乎无法测试
测试不稳定(Flaky) 同样的代码,不同时间跑,结果不一样 团队逐渐失去对测试的信任,甚至不再关注红绿

为什么测试会“依赖外部环境”

说白了,单元测试的核心原则是:只测我这一个类,不测别人的代码。你的UserService内部调了Retrofit,Retrofit又调了OkHttp,OkHttp再发网络请求。这一条链路下来,你根本没法控制中间哪个环节出问题。

我在一个电商项目里遇到过这种事。有一天所有测试突然红了,排查了两小时才发现是DNS解析出问题了——我们根本没改代码。你想想看,这种事多消耗团队精力。

核心原则:单元测试应该像实验室实验,所有条件可控制、可重复。Mock就是你的“实验环境搭建工具”。

Mock到底是什么

Mock,说白了就是造个假的服务替身。它不真的发网络请求,而是根据你预设的规则返回数据。这样你的测试就变成了:

  • 可重复——每次跑结果一样
  • 速度快——毫秒级返回,不用等网络
  • 可控——你想返回什么就返回什么,包括异常

举个例子,用Mockito写个简单的Mock测试:

@Test
public void testGetUserWithMock() {
    // 创建Mock对象
    ApiService mockService = mock(ApiService.class);
    
    // 预设返回数据
    User mockUser = new User(1, "张三");
    when(mockService.getUser(1)).thenReturn(Response.success(mockUser));
    
    // 执行测试逻辑
    User user = mockService.getUser(1).body();
    Assert.assertEquals("张三", user.getName());
}

你看,这段测试里没有任何网络请求,但业务逻辑该测的都测到了。而且它永远稳定——只要你不改代码,跑一百遍也是一样的结果。

Mock的几种常见实现方式

在Android领域,主流Mock方案有三类:

方案 原理 适用场景 缺点
Mockito/PowerMock 字节码代理,动态生成子类 Mock单个类或接口 对final/static方法支持不够好
MockWebServer (OkHttp) 启动本地Socket服务器返回预定义响应 模拟HTTP层的各种场景 需要启动轻量服务,略重
自定义Interceptor 在OkHttp拦截器层拦截请求并伪造响应 需要精细控制请求/响应流程 代码侵入性稍高

我个人习惯是:单元测试用Mockito,集成测试用MockWebServer。这个后面几章会展开讲。

传统测试还有什么坑

除了上面说的四个痛点,还有一个很隐蔽的问题——数据污染。我曾经在一个测试环境里跑自动化,测试往数据库写了一批脏数据,结果其他开发人员查问题时,被这些假数据误导了整整一下午。

另外,如果你依赖真实接口,你就没法做“先写测试、再写实现”的TDD流程。因为服务端还没写好,你怎么测?这直接限制了开发节奏。

注意:Mock不是万能的。它只能验证你代码的行为是否正确,不能验证真实接口的契约是否一致。因此我们还会需要集成测试和契约测试。这个后面也会讲。

这个章节的核心逻辑

总结一下网络测试的核心问题与Mock的位置,下面这张图能帮你看清整体脉络:

网络请求测试:传统 vs Mock 传统测试:直接请求真实API 痛点1:依赖外部环境 痛点2:测试速度慢 痛点3:难以覆盖边界场景 痛点4:测试不稳定(Flaky) Mock测试:模拟接口响应 优势:结果可重复 优势:毫秒级响应 优势:可控异常场景 优势:稳定可靠 核心原则:单元测试只测自身的业务逻辑,不测外部系统的行为。 Mock帮你把不可控的外部依赖,变成可控的“假对象”。 Mockito 字节码代理 MockWebServer 本地服务 自定义Interceptor

我的建议:刚开始接触Mock的同学,可以先从Mockito入手。它门槛最低,文档丰富,覆盖大部分场景。等用熟了,再根据项目需求切换到MockWebServer或自定义方案。

这一章的收获

  • 理解了为什么不能用真实API做单元测试——依赖、速度、覆盖率、稳定性四个维度都不行
  • 明白了Mock的本质:创建可控的测试环境,让测试只关注你的代码逻辑
  • 知道了三种常见的Mock实现方式,以及各自的适用场景
  • 看到了传统测试的潜在风险——数据污染、无法TDD、团队信任流失