- 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 有什么作用?
它从真实对象构建模拟,因此缺少属性和错误参数会引发错误,就像真实代码一样。
继续学习
需要测试、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%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker 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 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →