Skip to content

Mock 与 Stub

基础入门

Mock 和 Stub 都是测试替身技术,用于在测试中隔离被测代码对外部依赖的调用。测试替身的设计思想源自 Gerard Meszaros 的《xUnit Test Patterns》,核心目的是让测试更快速、更稳定、更可控。

Stub(桩)是最简单的测试替身,它只做一件事:在被调用时返回预设的固定值。Stub 不关心被调用了多少次、以什么参数调用,它只是被动地提供数据。\n\n比如一个返回用户信息的 Stub,无论传入什么用户 ID,都返回一个固定的用户对象。Stub 的特点是「只提供数据,不验证行为」。

Mock(模拟对象)则更进一步,它不仅能返回预设值,还能记录调用行为。Mock 可以验证方法是否被调用、调用次数、调用参数等交互信息。\n\n比如验证「支付服务的 pay 方法是否被调用了一次,且传入的金额参数是否正确」。Mock 的特点是「验证行为交互」。

两者的核心区别在于验证方式:Stub 用于状态验证,Mock 用于行为验证。使用 Stub 时,测试关注的是「方法返回了什么结果」。使用 Mock 时,测试关注的是「方法是如何被调用的」。面试时要清晰表达这个本质差异:Stub 提供数据让测试能继续执行,Mock 验证交互行为是否符合预期。

为什么重要

  • 隔离外部依赖,让单元测试不依赖数据库、网络、第三方服务,提高测试稳定性和执行速度。
  • 控制测试场景,能模拟各种边界情况(超时、异常、极端数据),覆盖真实环境难以构造的场景。
  • 降低测试成本,避免调用真实服务产生费用(如支付接口、短信网关)或污染生产数据。
  • 提高测试可重复性,消除外部环境的不确定性,让测试结果稳定可预期。
  • 面试高频考点,很多面试官会追问 Mock 和 Stub 的区别、使用场景和最佳实践,体现测试思维深度。

Mock vs Stub 的区别

Stub 的特点和适用场景是什么?

Stub 的特点是:只返回预设值,不记录调用信息,不验证交互行为。

适用场景包括:需要外部依赖返回固定数据的测试(如用户信息查询)、测试重点在内部逻辑而非外部交互、希望测试简单直接不需要复杂验证。

例如测试订单金额计算逻辑时,用 Stub 让用户服务返回固定用户信息,让积分服务返回固定积分,测试可以专注于金额计算。

Mock 的特点和适用场景是什么?

Mock 的特点是:能记录调用行为(次数、参数、顺序),用于验证交互是否正确,支持设置预期和断言。

适用场景包括:需要验证外部服务是否被正确调用、需要验证调用参数是否正确、需要验证调用次数或顺序、测试重点是「行为」而非「结果」。

例如测试支付流程时,用 Mock 验证支付网关是否被调用、调用参数是否正确、是否只调用了一次。

什么时候用 Stub,什么时候用 Mock?

判断原则是:如果测试重点是「内部逻辑是否正确」,用 Stub 提供数据让测试能执行。\n\n如果测试重点是「外部交互是否正确」,用 Mock 验证调用行为。

具体来说:测试计算逻辑、数据处理、业务规则时用 Stub。测试服务调用、消息发送、通知触发时用 Mock。

记住一句话:Stub 让测试能运行,Mock 让测试能验证。

前置知识

  • 单元测试基础:理解测试的目的、结构和编写方式,知道 AAA 模式(Arrange-Act-Assert)。
  • 依赖注入:理解如何将依赖作为参数传入,而非在代码内部直接创建,这是使用 Mock/Stub 的前提。
  • 接口和抽象类:理解面向接口编程,才能用 Mock/Stub 替换真实实现。
  • 测试框架:熟悉主流测试框架的 Mock 能力,如 Python 的 unittest.mock、Java 的 Mockito、JavaScript 的 Jest。
  • 测试分层概念:理解单元测试、集成测试、端到端测试的区别,知道 Mock/Stub 主要用于单元测试层。

学习路径

  • 第一阶段:理解概念。学习测试替身的分类(Dummy、Stub、Mock、Fake、Spy),理解每种替身的作用和区别。
  • 第二阶段:动手实践。用主流测试框架的 Mock 功能,练习简单的 Stub 场景(返回固定值)和 Mock 场景(验证调用)。
  • 第三阶段:项目应用。在真实项目中识别可 Mock 的依赖,实践依赖注入和测试隔离,积累常见场景的处理经验。
  • 第四阶段:深入原理。学习 Mock 框架的实现原理(如动态代理、字节码增强),理解 Mock 的边界和局限性。
  • 第五阶段:策略思考。思考 Mock 与真实测试的平衡,理解过度 Mock 的问题,建立分层的测试策略。

实操案例:接口 Mock 测试

场景:测试订单创建接口,需要验证支付服务调用是否正确。

Python 示例(使用 unittest.mock): from unittest.mock import Mock, patch

def test_create_order_calls_payment_service(): # 创建 Mock 对象 mock_payment = Mock() mock_payment.pay.return_value = {‘status’: ‘success’, ‘transaction_id’: ‘txn_123’}

# 注入 Mock
order_service = OrderService(payment_service=mock_payment)
# 执行测试
result = order_service.create_order(user_id=1, amount=100)
# 验证行为:支付服务被调用了一次
mock_payment.pay.assert_called_once()
# 验证调用参数
mock_payment.pay.assert_called_with(user_id=1, amount=100)

这个案例展示了 Mock 的典型用法:创建 Mock 对象,设置返回值,注入被测系统,验证调用行为。关键点是验证 pay 方法是否被调用、调用次数和参数是否正确,这是 Stub 做不到的。

实操案例:依赖隔离测试

场景:测试用户积分计算逻辑,需要隔离用户服务和积分服务。

Python 示例(Stub 场景): from unittest.mock import Mock

def test_calculate_user_points(): # 创建 Stub 对象,只提供数据 user_stub = Mock() user_stub.get_user.return_value = {‘id’: 1, ‘level’: ‘VIP’, ‘register_days’: 365}

points_stub = Mock()
points_stub.get_base_points.return_value = 100
points_stub.get_bonus_points.return_value = 50
# 注入 Stub
calculator = PointsCalculator(user_service=user_stub, points_service=points_stub)
# 执行测试,重点验证积分计算逻辑
result = calculator.calculate(user_id=1)
# 验证结果(状态验证,不验证 Stub 调用)
assert result == 200 # 100 + 50 + VIP加成(50)

这个案例展示了 Stub 的典型用法:外部依赖只提供固定数据,测试专注于验证积分计算逻辑是否正确。不关心 Stub 被调用了多少次,只关心最终结果是否正确。这就是状态验证的思路。

常见误区

误区一:把 Mock 和 Stub 概念混为一谈

很多人说「我用 Mock 做隔离」,实际用的是 Stub(只返回固定值)。正确理解:Mock 用于验证交互(如「支付服务被调用了一次」),Stub 用于提供数据(如「返回一个成功响应」)。面试时要举例说明两者的使用场景差异,展示对测试替身概念的清晰理解。

误区二:过度 Mock 导致测试失去意义

如果连业务逻辑本身都 Mock 了,测试就变成验证 Mock 配置而非验证真实代码。

正确做法是只 Mock 外部依赖(数据库、网络、第三方服务),业务逻辑保持真实。面试时要强调 Mock 的边界原则:隔离外部依赖,不隔离业务逻辑。

误区三:忽略 Mock 与真实实现的一致性

Mock 配置与真实实现不一致时,测试会通过但生产环境失败。

正确做法是:Mock 要与真实接口定义保持同步,可以通过契约测试或接口定义文件自动生成 Mock 配置来降低维护成本。定期用真实服务验证 Mock 的有效性。

误区四:测试中 Mock 太多依赖

每个依赖都 Mock 后,任何接口变化都需要更新大量 Mock 配置,维护成本极高。

正确做法是:关键路径用真实依赖(如集成测试),单元测试只 Mock 不稳定或不可控的依赖。区分哪些依赖应该 Mock(网络、数据库、第三方),哪些可以用 Fake 或真实实现。

误区五:不会区分 Spy 和 Mock

Spy 是在真实对象上记录调用信息的包装器,Mock 是完全模拟的假对象。

使用 Spy 时,真实的方法会被执行。使用 Mock 时,完全由 Mock 控制行为。

面试时要说明:需要部分真实行为时用 Spy,需要完全控制时用 Mock。

面试问答

单元测试里你什么时候用 Mock,什么时候用 Stub?

选择取决于「测试目的」和「验证重点」。

Mock 用于「验证交互行为」的场景:当需要验证外部依赖是否被正确调用时用 Mock,比如测试支付服务是否被调用了一次、调用参数是否正确。

Stub 用于「提供固定数据」的场景:当只需要外部依赖返回预设数据、不关心调用细节时用 Stub,比如测试订单金额计算逻辑时只需要支付服务返回成功状态。

判断方法:如果测试重点是「外部系统调用是否正确」,用 Mock。如果测试重点是「内部逻辑处理是否正确」,用 Stub。

过度 Mock 有什么问题,怎么避免?

过度 Mock 有三个问题:

第一,Mock 了不该 Mock 的东西,业务逻辑本身被 Mock 后测试就变成验证配置而非验证代码。

第二,Mock 配置与真实实现不一致,测试通过但生产失败。

第三,Mock 太多导致测试脆弱,任何接口变化都要更新大量 Mock。

避免方法:只 Mock 外部依赖不 Mock 业务逻辑。Mock 配置与真实接口定义保持同步(用契约测试或自动生成)。分层测试,单元测试用 Mock 保速度,集成测试用真实环境保正确性。

Mock 和真实测试环境怎么配合?

分层配合策略:单元测试层用 Mock 隔离外部依赖,保证测试快速执行、失败快速定位。集成测试层用真实环境验证依赖交互,验证 Mock 里假设的行为是否真实可行。端到端测试层用真实生产环境验证完整流程,验证用户真实体验。三层各有侧重,不是「要么全 Mock 要么全真实」,而是分层配合。

面试时要强调:Mock 是必要的隔离手段,但要控制 Mock 范围,关键业务逻辑的测试尽量用真实环境验证。

你用过哪些 Mock 框架,各自的特点是什么?

Python 用过 unittest.mock 和 pytest-mock,特点是轻量、与测试框架集成好,适合单元测试。

Java 用过 Mockito,特点是功能强大、支持注解驱动、与 Spring 集成方便。

接口层面用过 WireMock 和 MockServer,特点是独立部署、支持请求匹配和响应模板、跨语言通用。

前端测试用过 Jest 的 jest.fn() 和 Playwright 的 route.intercept。

选择依据:单元测试用语言原生 Mock 库(集成度高),接口 Mock 用独立服务(多语言通用),前端 Mock 用框架内置能力。

如何保证 Mock 与真实服务行为一致?

三个层面的保障:

第一,基于契约,使用 OpenAPI 等接口定义文件自动生成 Mock 配置,保证数据结构与真实一致。

第二,定期校验,在集成测试或预发布环境用真实服务验证 Mock 响应是否匹配,发现不一致及时修正。

第三,录制回放,使用录制工具(如 WireMock 的录制功能)捕获真实服务响应,生成 Mock 数据。

面试时要强调:Mock 是降低测试成本的手段,但必须与真实服务保持同步,否则会产生「测试通过但生产失败」的风险。