替身演员的艺术:pytest-mock 从入门到飙戏
文章目录
- 一、核心概念阐述
- 1、 什么是 Mock?
- 2、 什么是 `pytest-mock`?
- 二、直接⬆️操作
- 1、数据替换:`patch()`
- 示例场景
- 测试代码
- 知识点小结
- 2、数据替换:`patch.object()`
- 示例
- 参数说明
- 温馨小总结
- 3、数据监控:`spy()`
- 示例
- 注意事项
- 总结
- 4. 参数校验:`autospec`
- 5. 添加副作用:`side_effect`
- ① 抛出异常
- ② 返回可迭代对象
- ③ 重定义目标方法
一、核心概念阐述
1、 什么是 Mock?
在软件测试,尤其是单元测试(Unit Testing)中,我们经常会遇到这样的场景:
我们想要测试函数 A
,但函数 A
的正常运行依赖于另一个函数 B
(或者一个类、一个外部服务)。而调用真实的函数 B
可能会带来一些问题:
- 依赖复杂:函数
B
可能需要连接数据库、请求网络API或启动其他服务,导致测试环境的搭建变得非常复杂和耗时。 - 结果不稳定:函数
B
的返回结果可能受到网络波动、第三方服务状态等不可控因素的影响,导致测试结果时好时坏,产生“误报”(Flaky Tests)。 - 执行缓慢:真实的数据库查询或网络请求会大大拖慢测试的执行速度。
- 难以模拟边界情况:我们很难让真实的函数
B
返回一个“数据库连接超时”或“磁盘已满”的错误,来测试函数A
在这些异常情况下的处理逻辑。
为了解决这些问题,Mock(意为“模拟”或“模仿”)技术应运而生。
Mock 的核心思想是:创建一个“假的”函数 B
的替代品(我们称之为 Mock 对象)。这个 Mock 对象完全在我们的控制之下,我们可以让它:
- 不需要任何复杂的前置条件就能被“调用”。
- 在被调用时,立即返回我们预设好的、确定的数据。
- 模拟出各种我们想要的成功或失败的场景。
通过使用 Mock,我们可以将函数 A
与它的依赖项 B
隔离开来,从而能够独立、快速、稳定地测试函数 A
本身的逻辑是否正确,而无需关心函数 B
的真实实现。
简而言之,Mock 就是用一个可控的“替身演员”去代替测试中那些复杂的“配角”,从而让我们的测试焦点只集中在“主角”(被测对象)身上。
2、 什么是 pytest-mock
?
pytest-mock
是一个流行的 pytest
插件,它极大地简化了在 pytest
测试框架中使用 Mock 的过程。
虽然 Python 内置了强大的 unittest.mock
库用于实现 Mock 功能,但直接使用它有时会显得有些繁琐。pytest-mock
在 unittest.mock
的基础上做了一层优雅的封装,为开发者提供了更简洁、更符合 pytest
风格的接口。尤其是mocker
这个对象,它是一个fixture配置的来的,可以直接使用unittest.mock里面的工具,而不需要去导入unittest.mock。
二、直接⬆️操作
好的!我给你润色并稍作结构和语言优化,让说明更流畅、专业又不失亲切感,还会在 Markdown 语法上保持清晰,方便直接阅读和使用。下面是润色后的文章:
1、数据替换:patch()
在测试中,有些方法会依赖外部资源或有明显的延时。如果直接调用这些方法,不仅会拖慢测试速度,还可能因外部环境不稳定导致测试结果不可靠。
此时,我们可以使用 patch
将真实方法替换成一个“假的返回值”,让测试既快速又可控。
示例场景
假设有以下两个文件:
src/mod_a.py
:
# 外部模块可以创建这个对象,实现加法操作
import timeclass Worker:def do(self, x: int, y: int) -> int:# 模拟响应延迟---这个记住,后面会多次用到这个场景time.sleep(4)return x + y
src/mod_b.py
:
from src.mod_a import Worker, util# 对 Worker 的进一步封装,可以通过该函数间接调用 do 方法
def call_worker_add(a: int, b: int) -> int:worker = Worker()return worker.do(a, b)
现在我们的需求是:高效地测试 call_worker_add
方法。
问题在于:调用该方法会拖延 4 秒,因为它内部调用了 Worker.do
。
解决方法是:用 patch
替换掉 do
方法,让它立即返回一个指定的值。
测试代码
from src.mod_b import call_worker_add#安装了pytest-mock ,mocker这个对象pytest-mock会自动给我们注入,非常方便
def test_patch1(mocker):# mock_do 是一个“替身”,由 patch 返回mock_do = mocker.patch("src.mod_b.Worker.do")# return_value 指定我们想要的返回值(代替真实的 do)mock_do.return_value = 1314# 调用不会再延迟 4 秒,立即返回 1314assert call_worker_add(1213, 0) == 1314
知识点小结
-
patch()
的参数
接收一个字符串路径,表示要替换的目标方法的调用路径。 -
mock_do
的本质
mock_do
是一个MagicMock
对象,也就是原方法的“替身演员”。
通过它可以指定返回值 (return_value
) 或配置更多行为,测试起来灵活又快速。
2、数据替换:patch.object()
patch.object
和 patch
的作用几乎一样,只是写法更直接:通过对象本身去指定要替换的属性或方法。
示例
from src.mod_a import Worker
from src.mod_b import call_worker_adddef test_patch_object1(mocker):# 替换 Worker 的 do 方法mock = mocker.patch.object(Worker, 'do')mock.return_value = 1214# 调用时直接返回 1214assert call_worker_add(1213, 0) == 1214
参数说明
patch.object(target, attribute)
target
:需要修改的对象(类或实例)attribute
:对象中要被替换的属性名(方法或变量都可以)
温馨小总结
patch("路径.方法名")
:通过字符串路径来指定要替换的目标patch.object(对象, "属性名")
:通过对象和属性名来定位目标
两者效果一致,不同之处在于写法习惯和应用场景。
好的!下面是你这部分 pytest-mock 教程 的润色版本,我会保留技术细节,但让行文更清晰、更流畅,并加一点点轻松幽默的味道,方便读者理解。
3、数据监控:spy()
在单元测试中,我们有时候不仅想替换一个对象的方法,还想监控它的真实调用情况。此时,就可以使用 spy()
——它能够对某个属性或方法进行“旁观式监听”:
- 方法依旧会被真实执行;
- 调用信息(参数、次数)会被记录下来,方便后续断言。
简单来说,它就像个小本子:实际工作的人(方法)还是该干啥干啥,而小本子在旁边认认真真记账。
示例
from src.mod_a import Workerdef test_spy1(mocker):work = Worker()mock_do = mocker.spy(work, 'do')# 实际调用,执行时间会延迟4秒assert work.do(1213, 1) == 1214# 使用 spy 对调用信息进行断言:# 这里要求 do 必须只被调用过一次,# 且参数正好是 (1213, 1),否则测试失败mock_do.assert_called_once_with(1213, 1)
注意事项
在监控真实调用的基础上,我们还可以进一步切换到替换模式:
先让 spy
记录一次真实的调用情况,再通过 patch
来给方法指定一个快速返回值。这样一来,后续调用就可以避免原始的耗时逻辑。
def test_spy1(mocker):work = Worker()mock_do = mocker.spy(work, 'do')# 第一次调用:真实执行,耗时4秒assert work.do(1213, 1) == 1214mock_do.assert_called_once_with(1213, 1)# 监控完真实行为后,用 patch 替换返回值mock_do = mocker.patch("src.mod_b.Worker.do", return_value=1214)# 这次调用走的是 mock,不会再耗时 — 效率瞬间翻倍assert call_worker_add(1213, 1) == 1214
总结
spy()
= 记录真实调用(执行逻辑+参数追踪)。patch()
= 替换调用逻辑(直接返回指定结果)。- 组合使用 = 先看真实情况,再快速模拟结果。
这样做的好处是:既能保证我们验证了真实逻辑的调用方式,又能在需要的时候跳过耗时操作,保持测试高效。测试过程就像“侦探+替身演员”的完美配合。
好的!我来帮你润色这一部分,把逻辑讲解讲清楚,同时让语言更流畅生动,读者读起来更轻松。下面是改进后的版本:
4. 参数校验:autospec
来看一个常见的“坑”。假设我们写了这样一个测试:
def test_no_autospec(mocker):mock_do = mocker.patch("src.mod_a.Worker.do")# 设定返回值mock_do.return_value = 1214# 注意:这里随便传了一个参数,测试还是能过!# 但实际上 Worker.do(self, x, y) 要求有 3 个参数啊!assert mock_do(1) == 1214
结果测试 居然通过了。这就是个问题:被 mock 掉的方法已经完全不关心原函数的函数签名了,参数随便传都能跑,不小心就会产生“假测试”。
解决方案就是加上 autospec=True
。它会根据原函数的签名生成 mock,从而严格检查参数个数和参数名。像这样:
def test_autospec(mocker):mock_do = mocker.patch("src.mod_a.Worker.do", autospec=True)# 设定返回值mock_do.return_value = 1214# 参数不对,立刻报错,而不是假装通过assert mock_do(1) == 1214
这次就乖乖报错了,避免了测试的“假阳性”。
更细致的是,autospec
不仅会检查参数个数,连 参数名 也会严格对照。例如:
def test_autospec2(mocker):mock_do = mocker.patch("src.mod_a.Worker.do", autospec=True)mock_do.return_value = 1214# 原方法签名是 (self, x, y),这里写了 xx,就直接炸锅了assert mock_do(any, xx=1, y=2) == 1214
这样,参数写错名字也能被及时发现,从而让测试更可靠。
一句话总结:autospec 就是帮你避免“传错了还假装对”的情况。
5. 添加副作用:side_effect
side_effect
是个非常强大的选项,可以让 mock 的行为跟“静态返回值”不同,更加灵活。它有三大用法:
- 抛出异常
- 每次调用返回一个可迭代对象的下一个值
- 将整个方法替换为你自己定义的函数
① 抛出异常
有时我们要测试异常处理逻辑,可以用 side_effect
直接指定一个异常:
import pytest
from src.mod_b import call_worker_adddef test_side_effect_raise(mocker):side_effect = ValueError("哈哈哈")mock_do = mocker.patch("src.mod_b.Worker.do", side_effect=side_effect)# 如果没有捕获到 ValueError,将会报错with pytest.raises(ValueError):call_worker_add(1213, 1)
此时每次调用 Worker.do
,都会抛出这个 ValueError
。
② 返回可迭代对象
让 mock 在每次调用时返回不同的结果,非常适合测试“多次调用产生不同结果”的场景:
def test_side_effect_iterable(mocker):side_effect = [1, 2, "fd"]mock_do = mocker.patch("src.mod_b.Worker.do", side_effect=side_effect)assert call_worker_add(1213, 1) == 1assert call_worker_add(1213, 1) == 2assert call_worker_add(1213, 1) == "fd"
就像一个结果“轮盘”,每次转出来都不一样。
③ 重定义目标方法
如果要直接用自定义逻辑替换原方法,side_effect
也能帮上忙。比如我们想把 do(x, y)
改成做乘法:
def test_side_effect_override(mocker):def multiply(a, b):return a * bmock_do = mocker.patch("src.mod_b.Worker.do", side_effect=multiply)assert call_worker_add(3, 4) == 12
这样一来,Worker.do
就被替换成了我们自定义的 multiply
方法。
✨ 小总结:
autospec=True
→ 帮你严格校验调用签名,避免“假通过”。side_effect
→ 神器三连:抛异常、返回不同值、直接重写方法。
是不是感觉对 mock 的掌控力直接上升了好几个档次?😎