什么是 Mock:概念、行为验证 vs 状态验证、模拟对象本质、与真实对象的区别

核心概念

Mock(模拟对象)是测试夹具的一种特殊角色,它通过预先设定期望的交互行为,在测试执行过程中验证被测试对象是否正确地与外部依赖进行了通信。它不是真实对象的轻量替代品,而是一种“行为监听器”——关注“方法是否被调用、调用参数、调用次数”,而非依赖执行后的最终状态。

日常类比:审讯室录音笔
真实对象像一名自由谈话的嫌疑人,你可以听到他说的每一句话(状态);而 Mock 则像一只提前设定好磁带的录音笔,测试时只记录“是否按下了播放键”“按键时机是否准确”,根本不关心磁带内容是否正确。

行为验证 vs 状态验证

这是理解 Mock 角色的核心分水岭。两种验证哲学决定了测试夹具的设计方式:

维度 状态验证(Assert on State) 行为验证(Assert on Behavior)
关注点 被测试对象执行后的返回值或内部状态变化 被测试对象是否按照预期调用了外部依赖的方法
典型工具 JUnit、pytest、标准断言 Mockito、Mock、unittest.mock
验证时机 执行后检查 执行后检查调用记录(次数、参数、顺序)
适用场景 副作用可观测的纯函数或状态机 依赖外部服务、IO、消息队列等不可控交互
错误提示 “期望值 42,实际值 37” “期望调用 send() 1 次,实际调用 0 次”

行为验证的典型伪代码结构(以 Python unittest.mock 为例):

def test_send_order():
    mail_service = Mock()
    mail_service.send_email = MagicMock()

    order_processor = OrderProcessor(mail_service)
    order_processor.process(order_data)

    # 行为验证:只关心 send_email 是否被调用了 1 次
    mail_service.send_email.assert_called_once_with(
        to="user@example.com",
        subject="Order Confirmation"
    )

模拟对象的本质

Mock 对象本质上是“方法调用记录器 + 预定义行为代理”。其内部工作分为三层:

  1. 存根层(Stub):当调用某方法时返回预设的返回值(或抛出异常),保证测试不因外部失败而中断。
  2. 记录层(Recorder):记录每一次调用的方法名、参数、调用次数、调用顺序。
  3. 断言层(Asserter):提供 assert_called_with、assert_called_once 等 API,将记录与实际期望进行比对。

这种结构使得 Mock 能在“无副作用”的前提下验证对象间的协作协议,而非最终业务结果。

与真实对象的区别

对比项 真实对象 Mock 对象
执行逻辑 运行完整的业务代码、数据库查询、网络 IO 不执行任何业务逻辑,仅返回预设值或记录调用
测试速度 慢(毫秒~秒级) 极快(微秒级)
环境依赖 需要数据库、网络、文件系统等外部资源 完全隔离,不需要任何外部环境
验证维度 只能验证结果状态 可验证交互行为(调用次数、参数、顺序)
代码耦合 直接依赖具体实现类 依赖接口或抽象,通过依赖注入替换
维护成本 低(真实实现自然演变) 高(每次接口变更需同步更新 Mock 期望)
关键洞察:Mock 的核心价值不是替代真实对象,而是隔离交互层,让测试聚焦于“被测试对象是否正确地与邻居通信”。若测试需要验证业务逻辑结果,请使用 Fake 或真实对象;只有当协作行为本身是验证目标时,才启用 Mock。

常见误区

  1. 误将 Mock 当作桩(Stub):Mock 强调行为验证,Stub 只提供返回值。工具往往同时支持两者,但语义不同。
  2. 过度 Mock:在一个测试中 Mock 超过 3~4 个依赖,会使测试脆弱(每次接口变化都需同步调整期望),且丧失对业务逻辑的覆盖率。
  3. Mock 私有方法:Mock 应仅用于边界交互(外部服务、数据库、消息队列),不应 Mock 同一类内部的私有方法——那会破坏封装,且通常意味着设计需要重构。
  4. 忽视行为验证的副作用检查:行为验证无法捕获未预期但有害的额外调用(如重复发送邮件)。需结合“never call”期望或严格模式。

练习题

练习 2.1:判断下列场景应使用状态验证还是行为验证?

  1. 测试一个订单计算函数,验证总价是否等于商品单价 × 数量。
  2. 测试一个支付处理器,验证调用第三方支付服务的 charge() 方法时金额参数是否正确。
  3. 测试一个缓存组件,验证从缓存中读取数据时是否调用了底层存储的 get() 方法仅一次。

参考答案:① 状态验证;② 行为验证;③ 行为验证。

应用边界

Mock 最适用于以下场景:

  • 外部服务集成:支付网关、短信服务、邮件 API、第三方 REST/SOAP 接口。
  • 不可靠依赖:文件系统、网络套接字、硬件传感器、随机数生成器的调用时序。
  • 时序敏感协议:消息队列的 push/pull 顺序、WebSocket 的握手顺序。
  • 遗留代码接缝:当无法实例化真实对象(如需要容器、数据库连接池)时,用 Mock 隔离测试。

不应使用 Mock 的情况:

  • 被测试逻辑只需验证返回值或内部状态(应使用真实对象或 Fake)。
  • 测试与外部系统的集成端到端行为(应使用契约测试或真实沙箱)。
  • 依赖对象极其简单、无副作用(如 DTO、VO、静态工具类)。
Mock 行为验证与状态验证对比流程 行为验证 vs 状态验证 测试用例执行 调用真实对象方法 获取返回值/状态 断言:assertEqual(result, 42) 验证执行后的结果值 测试用例执行 调用 Mock 对象方法 Mock 记录调用信息 断言:assert_called_once() 验证交互次数、参数、顺序 两种验证哲学

上图清晰展示了两种验证方式的路径差异:状态验证遵循“执行→状态→断言”的数值链,而行为验证遵循“执行→记录→断言调用记录”的时序链。Mock 位于行为验证路径的核心,作为交互记录器而非逻辑提供者。