Python的mock把我坑惨了,原来Mock和patch的区别这么大

简介: 本文详解Python单元测试中`Mock`与`patch`的核心区别:`Mock`仅创建假对象,不自动替换;`patch`才负责在被测代码查找路径上临时替换并自动恢复。常见误区包括路径写错、混淆定义处与引用处、忽略`autospec`等,附7大典型坑点及正确用法示例。(239字)

小孙那天给一个外部API客户端写单元测试。代码大概长这样:

# api_client.py
import requests

def fetch_user(user_id):
   resp = requests.get(f"https://api.example.com/users/{user_id}")
   resp.raise_for_status()
   return resp.json()

测试的目标是:验证fetch_user能正确解析响应,而不真的去发网络请求。小孙写了这样一个测试:

# test_api_client.py
from unittest.mock import Mock
from api_client import fetch_user

def test_fetch_user():
   mock_response = Mock()
   mock_response.json.return_value = {"id": 1, "name": "Alice"}

   user = fetch_user(1)
   assert user["name"] == "Alice"

他跑了一下,测试确实通过了。但他打印了一下user,发现里面是真实的API返回数据——网络请求真的发出去了。他盯着代码看了半天:“我明明创建了mock_response,为什么fetch_user没用到它?”

问题出在他只是创建了一个Mock对象,但没有把它替换到requests.get的位置上。fetch_user里调用的是requests.get,而小孙的mock_response变量从来没有跟requests.get建立任何关系。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (19).png

他真正需要的是patch:在测试期间,把requests.get临时替换成返回mock_response的假函数。

“我以为创建了Mock,代码就会自动用它。原来Mock只是一个假对象,patch才是把假对象塞进去的动作。”

这个误解,是很多人刚接触unittest.mock时最常踩的坑。Mock和patch不是“两种mock方式”,它们是两个不同层面的东西:一个造零件,一个换零件。

先看一个最直观的对比

from unittest.mock import Mock, patch

# Mock:创建一个假对象
fake = Mock()
fake.get.return_value = "假数据"
print(fake.get())  # "假数据"

# patch:临时替换真实对象
with patch("requests.get") as mock_get:
   mock_get.return_value = "假数据"
   import requests
   print(requests.get("http://example.com"))  # "假数据"

# 出了 with 块,requests.get 恢复原样
print(requests.get)  # <function get at ...>,真实的函数

Mock是一个类,你实例化它,得到一个假对象。这个假对象可以响应任何属性访问、任何方法调用,返回你设定的值。但它只存在于你自己的变量里,不会影响任何真实代码。

patch是一个函数(同时也是装饰器和上下文管理器),它做一件事:在指定范围内,把某个真实对象替换成Mock,离开范围后恢复原样。

用一个生活场景来理解。

Mock是造了一把假钥匙。这把钥匙能开锁,但它只在你手里,门上的锁还是原来那把。

patch是趁没人的时候,把门上的锁临时换掉。进去拿完东西,再把原来的锁装回去。

区别一目了然:Mock造工具,patch换环境。

Mock:万能假对象

Mock的核心能力是“你说什么它都答应”。

from unittest.mock import Mock

m = Mock()

# 调用它
m()  # 返回一个 Mock 对象

# 访问属性
m.name  # 返回一个 Mock 对象
m.age   # 也返回一个 Mock 对象

# 调用方法
m.do_something(1, 2)  # 返回一个 Mock 对象

# 设置返回值
m.get.return_value = 42
print(m.get())  # 42

Mock默认会为任何属性访问创建一个子Mock。这是它的“万能”之处,也是它的“坑”之一:如果代码里有个拼写错误,Mock不会报错,它会默默返回一个新的Mock,测试可能照样通过,但实际代码是错的。

m = Mock()
m.get_user.return_value = {"name": "Alice"}

print(m.get_usr())  # 拼错了,但返回一个 Mock,不报错

Mock适合用来替代那些你完全不需要真实行为的协作者:外部API、数据库连接、文件系统、第三方服务。你只需要它“假装在那里”,并且按照你的设定返回数据。

patch:临时替换,离开恢复

patch的核心能力是“在指定范围内偷梁换柱”。

from unittest.mock import patch
import requests

with patch("requests.get") as mock_get:
   mock_get.return_value.status_code = 200
   mock_get.return_value.json.return_value = {"name": "Alice"}

   resp = requests.get("http://example.com")
   print(resp.json())  # {"name": "Alice"}

# 出了 with 块,requests.get 恢复成真实函数

patch的三种用法:

上下文管理器:with patch(...) as mock_obj:,适合单个测试里的临时替换。

装饰器:@patch("module.attr"),适合整个测试函数或测试方法。

手动启动/停止:p = patch(...); p.start(); ...; p.stop(),适合需要在setUp/tearDown里控制的场景。

无论哪种用法,patch的语义都是一样的:进入时替换,离开时恢复。它保证测试结束后,全局状态不会留下痕迹。

坑一:patch 的路径写错

这是patch最容易出错的地方,也是小孙最初没搞明白的地方。

patch的参数是“你要替换的那个名字在哪个模块里”,不是“这个东西定义在哪里”。

# api_client.py
import requests

def fetch_user(user_id):
   resp = requests.get(...)

这里api_client模块导入了requests,所以api_client的命名空间里有一个名字叫requests,指向requests模块。fetch_user调用的是api_client.requests.get。

所以patch的正确路径是:

@patch("api_client.requests.get")  # 替换 api_client 模块里的 requests.get
def test_fetch_user(mock_get):
   ...

而不是:

@patch("requests.get")  # 替换的是 requests 模块里的 get,可能不影响 api_client

为什么后者“可能”不影响?因为api_client在导入时,requests这个名字已经绑定到了requests模块对象上。patch("requests.get")确实替换了requests模块的get属性,如果api_client是通过requests.get访问的,理论上能看到替换。但这里有一个关键细节:patch替换的是当前时刻requests模块的get属性,而api_client里的requests指向同一个模块对象,所以api_client.requests.get也会看到替换。

那为什么还推荐patch("api_client.requests.get")?因为**如果代码用的是from requests import get**,情况就完全不同了:

# api_client.py
from requests import get

def fetch_user(user_id):
   resp = get(...)  # 用的是 api_client 命名空间里的 get

这时候patch("requests.get")无效,因为api_client里的get是导入时复制过来的独立引用,和requests.get已经没关系了。必须用patch("api_client.get")。

规则:patch 的目标是“被测试代码查找那个名字的地方”,不是“那个东西最初定义的地方”。 这是patch最核心的规则,也是最容易记错的一条。

坑二:以为 Mock() 会自动替换

mock_get = Mock()
mock_get.return_value.json.return_value = {"name": "Alice"}

# 以为 fetch_user 会用 mock_get,实际上不会
user = fetch_user(1)

Mock()只是创建了一个对象,赋值给变量mock_get。fetch_user内部调用的是requests.get,和mock_get没有任何关系。要让fetch_user用上这个Mock,必须用patch把它放到requests.get的位置上。

with patch("api_client.requests.get", return_value=mock_get.return_value):
   user = fetch_user(1)

或者更常见的写法:

with patch("api_client.requests.get") as mock_get:
   mock_get.return_value.json.return_value = {"name": "Alice"}
   user = fetch_user(1)

patch返回的mock_get就是替换上去的那个Mock对象。你在这个对象上设返回值,被测代码调用requests.get时拿到的就是你的设定。

坑三:patch 装饰器的参数顺序

用@patch装饰器时,Mock对象会作为参数传给测试函数。如果有多个@patch,参数的顺序是从下往上对应的。

@patch("module.func_b")
@patch("module.func_a")
def test_something(mock_a, mock_b):
   ...

最下面的@patch("module.func_a")对应第一个参数mock_a,上面的@patch("module.func_b")对应第二个参数mock_b。

这个顺序很容易记反。写多个@patch时,最稳妥的方式是给每个Mock起个明确的名字,然后对照装饰器的顺序检查一遍。

坑四:patch 了错误的层级

# service.py
import database

def get_user(user_id):
   return database.query(f"SELECT * FROM users WHERE id = {user_id}")

要测试get_user,需要patch database.query。但如果你写:

@patch("service.database.query")
def test_get_user(mock_query):
   ...

这是对的,因为service模块导入了database,service.database指向database模块,service.database.query就是database.query。

但如果你写:

@patch("database.query")
def test_get_user(mock_query):
   ...

也可能对,取决于service是怎么导入的。如果service是import database,那么service.database和database是同一个模块对象,patch任何一个都能生效。但如果service是from database import query,那么service.query是一个独立的引用,必须patch service.query。

最安全的做法:patch service.database.query,因为这是被测代码实际查找的路径。即使service用的是from database import query,patch("service.database.query")也能生效(如果service.database还存在的话),但更准确的写法是patch("service.query")。

规则还是那一条:patch 被测代码查找名字的位置。

坑五:忘记 autospec,Mock 太“宽容”

@patch("api_client.requests.get")
def test_fetch_user(mock_get):
   mock_get.return_value.json.return_value = {"name": "Alice"}
   ...

这个测试能跑,但mock_get没有spec,它可以接受任何参数、返回任何东西。如果被测代码调用requests.get时传错了参数,测试不会发现。

# 被测代码写错了,传了错误的关键字参数
resp = requests.get(url, timout=10)  # 拼写错误,应该是 timeout

# 测试照样通过,因为 mock_get 接受任何参数

autospec=True会让Mock根据真实函数的签名来限制参数:

@patch("api_client.requests.get", autospec=True)
def test_fetch_user(mock_get):
   ...

这样mock_get就只能接受requests.get真实支持的参数,传错参数会直接报TypeError,测试能发现这类错误。

autospec的代价是配置稍微麻烦一点,但它能抓住“参数拼写错误”“参数数量错误”这类Mock默认会放过的bug。

坑六:patch 没有清理,污染其他测试

用patch的start()/stop()手动控制时,如果stop()没被调用(比如测试中途抛异常),替换会一直生效,影响后续测试。

def test_something():
   p = patch("module.func")
   mock_func = p.start()
   # 如果这里抛异常,p.stop() 不会执行
   do_something()
   p.stop()

解决方案是用try/finally,或者直接用with/装饰器,它们会自动清理。

def test_something():
   with patch("module.func") as mock_func:
       do_something()
   # 自动 stop

with和装饰器在异常时也会执行清理,这是它们比手动start()/stop()更安全的地方。

坑七:patch 内置函数或模块属性

patch可以替换很多东西,但不是所有东西都能替换。

能替换的:模块里的函数、类、类的方法、模块属性、sys.modules里的模块。

不能替换或很难替换的:内置函数(如len、print)、C扩展里的函数、不可变对象的属性。

# 替换内置函数,通常不推荐
@patch("builtins.print")
def test_something(mock_print):
   ...

替换内置函数会影响整个进程,包括测试框架自己的输出。除非确实需要,否则不要patch内置函数。

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

判断标准很清晰:

需要一个假对象,作为参数传给被测代码:用Mock()。

def test_process():
   fake_db = Mock()
   fake_db.query.return_value = [{"id": 1}]
   result = process(fake_db)  # 把假对象传进去
   assert result == ...

需要替换被测代码内部引用的某个真实对象:用patch()。

@patch("service.database.query")
def test_get_user(mock_query):
   mock_query.return_value = [{"id": 1}]
   result = get_user(1)  # get_user 内部调用 database.query
   assert result == ...

两者结合:用patch替换,用Mock作为返回值或副作用。

@patch("service.database")
def test_get_user(mock_db):
   mock_db.query.return_value = [{"id": 1}]
   ...

Mock是“造一个假对象”,patch是“把假对象放到真实代码会去找的位置上”。两者解决的是不同的问题,经常一起用,但不能互相替代。

回到小孙的故事

小孙最终的测试是这样的:

from unittest.mock import patch
from api_client import fetch_user

@patch("api_client.requests.get", autospec=True)
def test_fetch_user(mock_get):
   mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}

   user = fetch_user(1)

   assert user["name"] == "Alice"
   mock_get.assert_called_once_with("https://api.example.com/users/1")

patch把api_client.requests.get替换成了Mock,fetch_user调用时拿到的是假响应,不会发真实网络请求。autospec=True保证调用参数和真实requests.get的签名一致。assert_called_once_with验证了调用参数正确。

他后来在团队文档里写了一条:“Mock是造一个假对象,不会自动替换任何东西。patch是把假对象放到真实代码查找的位置上,离开时恢复。patch的路径写‘被测代码查找名字的地方’,不是‘东西定义的地方’。多个@patch参数从下往上对应。尽量加autospec=True,让Mock的签名和真实函数一致。”

这个区分看起来简单,但真到写代码的时候,很多人会创建了Mock却忘了patch,然后发现网络请求真的发出去了;或者patch了错误的路径,测试通过了但实际代码没被替换;或者patch了requests.get但代码用的是from requests import get,替换根本不生效。

记住那个换锁的比喻:Mock是造一把假钥匙,patch是趁没人把门上的锁临时换掉。你要让假钥匙起作用,得先把锁换了。换完记得换回来,否则别人用原来的钥匙就打不开门了。

Mock和patch本身不难,难的是想清楚“被测代码到底在哪个命名空间里查找这个名字”。想清楚了,patch的路径就不会写错,Mock也不会白造。

目录
相关文章
|
10天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
3995 19
|
11天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
4135 49
|
11天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1976 7
|
6天前
|
云安全 人工智能 安全
|
6天前
|
存储 弹性计算 人工智能
2026 年阿里云双十一活动入口攻略,新客服务器优惠通道、三大会场入口梳理
综合来看,2026年阿里云双十一活动入口清晰: 阿里云轻量应用服务器、阿里云ECS云服务器、阿里云GPU云服务器、阿里云百炼大模型服务平台 活动会场、综合权益中心、99计划专场三大入口覆盖服务器、AI权益与续费特价;新用户通过控制台首页banner、权益中心、产品页三处路径进入专属专场,老用户通过控制台弹窗和费用中心权益中心进入老客专区,企业用户做企业实名后解锁专属满减与阶梯折扣。
819 0
|
5天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
1087 2
|
12天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
2144 6
|
云安全 安全 网络安全
|
24天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
4337 58