Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

  1. 首页
  2. 博客
  3. 测试自动化

测试自动化

Python 中的模拟:unittest.mock、补丁和导致测试谎言的错误

模拟可以让您在没有真实网络、数据库或时钟的情况下测试代码。如果使用不小心,它们也会让损坏的代码通过。以下是如何在运行每个示例时很好地使用unittest.mock。

阅读约 8 分钟
Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

要点速览

  • mock 代表真实对象,例如 API 客户端,因此测试可以快速且离线运行。
  • 使用 return_value 表示固定答案,使用 side_effect 表示错误或序列。
  • 在使用名称的地方修补名称,而不是在定义名称的地方。这是最常见的错误。
  • 使用 spec 或 autospec,这样拼写错误和错误的参数就会失败,而不是默默地传递。

您的函数调用天气 API。您的测试不应依赖于互联网、API 密钥或明天的天气。 Python 的内置 unittest.mock 允许您用一个准确返回您选择的内容的替代调用来替换该调用。

我们编写了一个小型 weather 模块和九个测试,全部使用 Python 3.13 的单元测试运行。所有九个都通过了,下面的打印输出来自该运行。

为什么要嘲笑?

  • 速度:无需网络调用或数据库设置。
  • 可靠性: 测试不会因为第三方服务关闭而失败。
  • 控制:您可以按需模拟超时、错误和奇怪数据。
  • 隔离:你测试的是你的逻辑,而不是别人的服务器。

模拟主要属于单元测试。集成测试仍然应该与真实的或现实的依赖关系进行对话。

模拟基础知识

from unittest.mock import Mock

api = Mock()
api.get_price.return_value = 499

print(api.get_price("pen"))                # 499
api.get_price.assert_called_once_with("pen")
print(api.get_price.call_args_list)        # [call('pen')]
print(api.anything_at_all())               # <Mock name='mock.anything_at_all()' ...>

A Mock 记录每个呼叫,以便您可以检查它是如何使用的。不过请注意最后一行。一个普通的 Mock 可以发明您需要的任何属性。正如我们将看到的,这种灵活性很方便,但也很危险。

MagicMock 是相同的想法,但也支持像 __len__ 和 __iter__ 这样的“神奇”方法。 patch()默认使用MagicMock。

side_effect:错误和序列

side_effect 控制每次调用时发生的情况。将其设置为异常以模拟失败:

api.fetch.side_effect = ConnectionError("down")
api.fetch()   # raises ConnectionError

将其设置为列表以在连续调用时返回不同的结果。这非常适合测试重试逻辑:

def fetch_with_retry(api, attempts=3):
    for _ in range(attempts):
        try:
            return api.fetch()
        except ConnectionError:
            continue
    raise ConnectionError("all attempts failed")

api = Mock()
api.fetch.side_effect = [ConnectionError("busy"), {"ok": True}]
print(fetch_with_retry(api))    # {'ok': True}
print(api.fetch.call_count)     # 2

第一次调用失败,第二次成功,测试证明该函数恰好重试了一次。针对真实服务进行模拟几乎是不可能的。

patch():临时替换一些东西

通常,依赖项不会传入。它是在模块内部导入的。 patch() 在测试期间将其交换并在测试后恢复:

# weather.py
from http_client import get as requests_get

def report(city):
    data = requests_get(f"https://api.example.com/weather?city={city}")
    return f"{city}: {data['temp_c']}°C"
from unittest.mock import patch

with patch("weather.requests_get") as fake_get:
    fake_get.return_value = {"temp_c": 31}
    print(weather.report("Pune"))   # Pune: 31°C
    fake_get.assert_called_once_with(
        "https://api.example.com/weather?city=Pune")

您可以使用 patch 作为这样的上下文管理器,或者作为装饰器,它将模拟作为参数传递。

在使用的地方打补丁,而不是在定义的地方打补丁

这是几乎每个人都会犯的错误。真正的 get 函数位于 http_client 中,因此在那里修补它感觉很自然:

with patch("http_client.get") as fake_get:
    fake_get.return_value = {"temp_c": 99}
    weather.report("Pune")

# ConnectionError: no network in tests: https://api.example.com/weather?city=Pune

真正的函数运行了。当weather.py执行from http_client import get as requests_get时,它创建了自己的名称来指向原始函数。修补 http_client.get 更改了不同的名称,因此 weather 从未见过模拟。我们的http_client.get故意加注,所以错误很大。在真实的项目中,它可能会悄悄地调用实时 API。

规则:对被测模块中的名称进行补丁,写为"module_under_test.name"。如果模块执行了 import http_client 并调用了 http_client.get(),则修补 "http_client.get" 是正确的,因为查找发生在调用时。

spec 和 autospec:使模拟严格

普通的 Mock 接受任何内容,包括拼写错误。通过规范,它只允许真实类具有的属性:

api = Mock(spec=weather.WeatherClient)
api.get_temprature("Pune")
# AttributeError: Mock object has no attribute 'get_temprature'

loose = Mock()
loose.get_temprature("Pune")    # returns a Mock, no error at all

松散的模拟很高兴地接受了拼写错误的方法,因此使用它的测试可以通过,即使真实的代码会崩溃。 autospec 进一步检查方法签名:

with patch("weather.WeatherClient", autospec=True) as Fake:
    client = Fake.return_value
    client.get_temperature("Pune", "extra", "args")
# TypeError: too many positional arguments

在模拟您自己的类时,首选 autospec=True 或 create_autospec()。当真实代码发生变化时,它可以使模拟保持诚实。

模拟日期和时间

取决于今天日期的代码很难测试。将时钟包装在一个小函数中并修补:

def today():
    return datetime.date.today()

def is_weekend():
    return today().weekday() >= 5

@patch("weather.today")
def test_date(self, fake_today):
    fake_today.return_value = datetime.date(2026, 11, 7)
    print(weather.is_weekend())   # True (a Saturday)

您无法轻松地直接修补 datetime.date.today,因为内置类型不允许设置其属性。 freezegun 和 time-machine 等库可以更全面地处理这个问题。

关于模拟的断言

方法 支票
assert_called() 至少打过一次电话
assert_called_once() 只调用一次
assert_called_with(...) 最后一次调用使用了这些参数
assert_called_once_with(...) 带有这些参数的一次调用
assert_any_call(...) 任何调用都使用这些参数
assert_not_called() 从未打电话过

当断言失败时,消息是明确的。我们在一个从未运行过的方法上调用了 assert_called_once():

AssertionError: Expected 'save' to have been called once. Called 0 times.

较旧的 Python 版本会让拼写错误的断言方法(例如 assret_called_once)默默地通过。现代版本会针对以 assert 开头的名称或其常见拼写错误引发错误,但规范仍然是更强大的保护措施。

pytest 和模拟

上面的所有内容也适用于 pytest。许多 pytest 用户还使用内置的 monkeypatch 固定装置进行简单替换,或使用 pytest-mock 插件,该插件提供了自动撤消补丁的 mocker 固定装置。 “使用补丁的地方”规则适用于所有这些。有关固定装置和设置,请参阅我们的 pytest 教程。

什么时候不应该嘲笑

  • 不要模拟您正在测试的内容。如果模拟完成了所有工作,则测试无法证明任何内容。
  • 避免模拟简单的值对象。使用真实的东西。
  • 不要模拟一切。每当重构代码时,充满模拟的测试经常会中断,即使行为是正确的。
  • 进行一些真正的集成测试。 模拟对您对服务的假设进行编码,而这些假设可能是错误的。

模拟还可以轻松地扩大代码覆盖率,因为模拟路径运行时背后没有任何实际行为。

常见的嘲笑错误

  • 修补错误的名称。 如上所述,仍然是“我的模拟被忽略”的首要原因。
  • 忘记设置return_value。然后,被测试的代码接收到一个Mock对象而不是数据。它可能会直接通过您的函数并意外地使测试通过,例如当您仅检查结果是否真实时。
  • 对模拟而不是结果进行断言。 检查 save() 是否被调用比检查保存的内容弱。将 assert_called_once_with(...) 与实参一起使用,或者更好的是,对函数的结果进行断言。
  • 泄漏补丁。 调用 patcher.start() 而不使用 stop() 会留下假的以供以后测试。使用上下文管理器、装饰器或addCleanup(patcher.stop)。
  • 堆叠装饰器的顺序错误。 对于多个 @patch 装饰器,底部的装饰器将作为第一个参数传递。默默地混合它们会配置错误的模拟。
  • 在许多测试中模拟不属于您的库。将其包装在您自己的一个小类中并嘲笑它。然后,您的测试将描述您的需求,而不是库的 API。

模拟、存根和赝品

这些词经常被宽松地使用,但是在讨论测试时这些差异很有帮助。 stub 仅返回预设答案。 mock 还会记录调用,以便您可以验证它的使用方式。 fake 是一种有效的、简化的实现,例如内存存储库而不是数据库。 Python 的 Mock 可以充当存根或模拟。假货是您自己编写的普通类,它们通常会导致能够更好地进行重构的测试,因为它们检查行为而不是确切的调用。

一个好的经验法则是模拟系统的边缘:网络、时钟、随机数、文件系统和付费第三方服务。在这些边缘内,更喜欢真实的物体。以这种方式构建的测试保持快速,并且它们仍然执行您实际编写的代码。

最后的话

模拟是一种精确的工具:替换缓慢或不可预测的依赖项,控制它们返回的内容,并检查代码如何使用它们。有两个习惯可以预防大多数问题。修补被测模块中的名称,并使用 autospec,以便您的模拟拒绝真实对象将拒绝的调用。

常见问题

Python 中什么是模拟?

模拟会用您在测试期间控制的假对象替换真实对象,例如 API 客户端或数据库。 Python 将其包含在 unittest.mock 模块中。

Mock 和 MagicMock 有什么区别?

MagicMock 是一个 Mock,还支持 __len__、__iter__ 和 __enter__ 等魔术方法。 patch() 默认创建一个 MagicMock。

为什么我的补丁不起作用?

通常是因为您修补了对象的定义位置而不是使用对象的位置。修补被测模块内的名称。

return_value 和 side_effect 有什么区别?

return_value 设置固定结果。 side_effect 可以引发异常、按顺序从列表中返回值或调用函数。

autospec 有什么作用?

它从真实对象构建模拟,因此缺少属性和错误参数会引发错误,就像真实代码一样。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →