单元测试中的 Mock 和 Stub 区别

FreeGuideOnline 16阅读 2026-07-08

单元测试中的 Mock 和 Stub 区别

在编写单元测试时,我们经常需要隔离被测代码的依赖,以保证测试的稳定性、速度和可重复性。这时就需要使用 测试替身 (Test Doubles) 来替代真实依赖。最常见的两种测试替身就是 Stub 和 Mock。初学者往往容易混淆它们,但它们的目的和使用方式截然不同。本文将帮你彻底理清这两者的区别,并通过 Python 代码示例让你轻松掌握。

什么是测试替身?为什么需要它们?

单元测试的目标是验证“单元”本身的逻辑,而不是它所依赖的外部系统。如果被测代码依赖于数据库、网络服务、文件系统或其他模块,这些依赖可能带来以下问题:

  • 运行缓慢(例如真实网络请求)
  • 环境依赖强(需要特定数据库可用)
  • 行为不稳定(返回数据可能变化)
  • 难以模拟边缘情况(如网络超时、权限错误)

测试替身 就是用来替代这些真实依赖的轻量级对象,让我们能够完全控制被替代组件的行为。常见的测试替身包括 Dummy、Fake、Stub、Mock、Spy。其中 StubMock 是日常工作中使用最频繁的两种,也是我们需要重点区分的角色。

Stub(桩):只提供预设数据,不关心如何被调用

Stub 是一个“哑”对象,它只提供我们预先设定好的返回值,不关心自己被调用了多少次、用什么参数调用。它的唯一目的就是让被测代码能够顺利执行下去,获取所需的值

Stub 的核心特征

  • 提供返回值: 对特定的方法调用返回预设的固定结果。
  • 不验证交互: 不会记录或检查它被调用的细节。
  • 状态验证为主: 测试最终通过验证被测代码的状态(某个对象的属性、返回值等)来判断是否正确。

Stub 示例

假设有一个 WeatherService 类,它从外部 API 获取天气信息。我们想测试 OutfitRecommender 类,它根据天气返回穿衣建议。

# 被测代码
class OutfitRecommender:
    def __init__(self, weather_service):
        self.weather_service = weather_service

    def recommend(self, city):
        temp = self.weather_service.get_temperature(city)
        if temp > 25:
            return "T-shirt"
        else:
            return "Jacket"

在测试中,我们不想真的调用外部 API,于是创建一个 Stub 来替代 weather_service

from unittest import TestCase
from unittest.mock import MagicMock

class TestOutfitRecommender(TestCase):
    def test_recommend_cold_weather(self):
        # 创建一个 Stub 替身
        stub_service = MagicMock()
        # 设定 Stub 的行为:无论收到什么参数,都返回 10 度
        stub_service.get_temperature.return_value = 10

        recommender = OutfitRecommender(stub_service)
        result = recommender.recommend("Beijing")

        # 验证最终状态(返回值)是否正确
        self.assertEqual(result, "Jacket")
        # 注意:这里并没有验证 stub_service 被如何调用!

在这个例子中,stub_service.get_temperature.return_value = 10 让 Stub 在任何调用下都返回 10。我们的测试只关心 recommend() 在低温下是否返回 “Jacket”,完全不关心 Stub 的调用细节。这就是典型的 Stub 用法。

Mock(模拟):不仅提供假数据,更要验证交互行为

Mock 是比 Stub 更“聪明”的测试替身。它也能预设返回值(像 Stub 一样),但更重要的是,它会记录自己被调用的方式,并允许我们在测试中验证这些调用是否按预期发生。Mock 关注的是行为交互,即被测代码是否以正确的方式与依赖进行了通信。

Mock 的核心特征

  • 提供返回值(可选): 同样可以预设返回值,但它不是 Mock 存在的首要目的。
  • 验证交互: 测试会明确检查 Mock 对象是否被正确调用了(调用次数、参数、调用顺序等)。
  • 行为验证为主: 测试通过验证被测代码对外部依赖的调用行为来确保逻辑正确。

Mock 示例

考虑一个 UserRegistration 类,它需要保存用户并发送通知邮件。我们关心的是:当调用 register() 时,是否确实调用了send_email() 方法,并且参数是正确的。

class UserRegistration:
    def __init__(self, user_repo, email_service):
        self.user_repo = user_repo
        self.email_service = email_service

    def register(self, username, email):
        self.user_repo.save(username, email)
        # 核心交互:必须发送一封欢迎邮件
        self.email_service.send_email(email, "Welcome!")

测试代码将使用 Mock 来替代 email_service,并验证 send_email 是否被正确调用。

from unittest import TestCase
from unittest.mock import MagicMock

class TestUserRegistration(TestCase):
    def test_register_sends_welcome_email(self):
        # 创建 Mock 替身
        mock_email_service = MagicMock()
        mock_user_repo = MagicMock()

        registration = UserRegistration(mock_user_repo, mock_email_service)
        registration.register("alice", "[email protected]")

        # 验证交互行为:send_email 被调用了一次,且参数正确
        mock_email_service.send_email.assert_called_once_with(
            "[email protected]", "Welcome!"
        )
        # 也可以不关心返回值,只关心是否被调用

这里的关键在于 assert_called_once_with,它明确验证了交互行为。如果我们的代码忘记调用 send_email 或参数错误,测试会立刻失败。而 mock_user_repo 也是一个 Mock(我们可能也会验证 save 是否被调用),但如果你只关心返回值而不验证调用,它就可以被视为一个 Stub。

Stub 与 Mock 的关键区别

虽然很多时候我们用同一个库(如 Python 的 unittest.mock 或 Java 的 Mockito)创建这两种替身,但它们在目的和验证方式上有本质区别。

维度 Stub Mock
主要目的 提供测试所需的间接输入,让代码顺利运行 验证被测代码是否以正确方式与依赖交互
验证方式 状态验证:检查被测代码的最终结果 行为验证:检查依赖是否被正确调用
关心什么 被测代码“得到了什么” 被测代码“做了什么”
典型断言 assertEqual(result, expected_value) assert_called_with(...), assert_called_once()
是否关心调用细节 完全不关心 高度关心(参数、次数、顺序)
失败原因 返回值计算错误、逻辑分支错误 未发生预期的调用、参数错误、调用次数不对

简单记忆口诀:

  • 当你只关心 “从依赖那里拿回什么值” 时,用 Stub
  • 当你需要确保 “依赖的某个方法是否被正确触发” 时,用 Mock

如何选择:Stub 还是 Mock?

在实际开发中,一个测试可能同时需要两种行为,但遵循以下原则可以帮你写出更清晰、可维护的测试。

优先使用 Stub(状态验证)的情况

  1. 返回值是后续计算的输入,比如查询数据库获得用户信息,再基于该信息做处理。你只想要一个假用户,不关心查询方法被调用了几次。
  2. 被测方法的最终产出就是返回值或对象状态变化,而不是对外部的命令调用。
  3. API 本身就像是一个“数据提供者”,例如 get_price(), fetch_config()

必须使用 Mock(行为验证)的情况

  1. 被测代码需要向外部系统发送命令,且没有返回值可供观察。例如发送邮件、记录日志、推送消息。如果不验证行为,就无法知道命令是否发出。
  2. 需要确保重要的交互顺序或精确参数,例如“必须先保存订单再扣减库存”。
  3. 依赖是副作用发生器,你不能通过查询去验证副作用发生了,只能通过验证调用。

警惕过度使用 Mock:
如果测试中充满了大量的 assert_called_with,会使得测试与实现细节高度耦合,未来任何内部重构都可能导致测试失败,即使行为完全正确。尽量用 Stub 验证最终结果,只在必须验证交互的地方使用 Mock。记住:测试行为,而不是实现

实践建议与常见陷阱

  1. 角色可以重叠,但意图要清晰
    同一个 MagicMock 对象可以既设置 return_value(充当 Stub),又使用 assert_called_with(充当 Mock)。但为了代码可读性,最好让一个对象在测试中只扮演一种清晰的角色。如果既需要返回值又需要验证调用,可以明确命名变量为 mock_email_servicestub_price_api

  2. 不要用 Mock 验证自己的业务逻辑
    如果被测方法返回了一个值,优先使用 Stub + 状态断言来验证业务逻辑。例如,不要 Mock 掉 list.sort() 然后验证它被调用了,而应该检查列表本身是否有序。

  3. Stub 不应当让测试失败
    Stub 只提供数据,它本身的设计初衷就是“永远工作”。如果你需要模拟依赖抛出异常,可以使用 stub_service.get_temperature.side_effect = Exception("API down"),这仍然是 Stub 的一种预设行为,但测试关注的并不是“是否调用了温度服务”,而是“当温度服务挂掉时,我们的代码是否健壮”。

  4. 经典的学校比喻
    想象老师(测试)要考察学生(被测代码)的计算能力:

    • Stub:老师直接告诉学生“假设当前温度是10度”,然后看学生得出的穿衣建议是否正确。完全不关心学生问了老师几次。
    • Mock:老师要求学生必须向图书馆发送一份借书申请单,然后检查学生是否真的去做了,并且申请书内容正确。

总结

角色 定义 典型断言语
Stub 为测试提供间接输入的假对象,不记录调用详情 assert result == "Jacket"
Mock 记录调用细节,用于验证行为交互的假对象 mock.assert_called_once_with("[email protected]", "Welcome!")

理解 Stub 和 Mock 的区别,是写好单元测试、构建稳定可维护测试套件的关键一步。下次写测试时,先问自己:

  • 我是在给被测代码 “喂数据”(Stub),还是在检查 “它有没有正确使唤别人”(Mock)?
  • 这个测试最好通过验证什么来保证正确性?是最终产出,还是交互过程

答案将直接告诉你该用哪种替身。希望这篇教程能让你彻底告别 Mock/Stub 的混淆,写出更强大、更清晰的单元测试。