惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
爱范儿
爱范儿
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 叶小钗
C
Check Point Blog
F
Fortinet All Blogs
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
博客园 - 【当耐特】
Jina AI
Jina AI
T
The Blog of Author Tim Ferriss
P
Palo Alto Networks Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LINUX DO - 热门话题
M
MIT News - Artificial intelligence
Vercel News
Vercel News
博客园 - 司徒正美
Recorded Future
Recorded Future
阮一峰的网络日志
阮一峰的网络日志
P
Proofpoint News Feed
P
Privacy & Cybersecurity Law Blog
Webroot Blog
Webroot Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
云风的 BLOG
云风的 BLOG
D
DataBreaches.Net
Y
Y Combinator Blog
J
Java Code Geeks
B
Blog
A
About on SuperTechFans
O
OpenAI News
aimingoo的专栏
aimingoo的专栏
T
Tor Project blog
Stack Overflow Blog
Stack Overflow Blog
月光博客
月光博客
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - Franky
AWS News Blog
AWS News Blog
GbyAI
GbyAI
Application and Cybersecurity Blog
Application and Cybersecurity Blog
IT之家
IT之家
V
V2EX
量子位
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
Help Net Security
Help Net Security
W
WeLiveSecurity
C
Cyber Attacks, Cyber Crime and Cyber Security

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
读:Python mock/patch 用法与陷阱 - 暗无天日
lujun9972,Claude Code · 2026-06-22 · via 暗无天日

目录

  • 假绿测试, 测试通过但 mock 没干活
  • 怎么证明 mock 真的拦住了
  • patch 的位置决定成败
  • mock 的另一个用途, 制造碰撞
  • 比 mock 更好的办法, 显式 override 参数
  • 什么时候别用 mock

原文链接(Bob Belderbos)

我之前写过 Python Mock 第三方依赖的四种策略, 讲怎么用 patchMagicMock 替换不同类型的依赖。那篇文章讲的是「怎么 mock」, 但没回答几个更基本的问题, mock 到底生效了没有? patch 位置错了会怎样? 有些场景是不是根本不该用 mock? 这篇博文顺着这条线展开。

假绿测试, 测试通过但 mock 没干活

文章从一个真实的 case 讲起。有个函数调用汇率 API 做货币转换,API 返回失败就抛出 CurrencyConversionError

from decimal import Decimal

class CurrencyConversionError(Exception):
    pass

def convert_currency(amount, from_currency, to_currency):
    if from_currency == to_currency:
        return amount
    import requests
    response = requests.get(
        f"https://v6.exchangerate-api.com/v6/KEY/pair/{from_currency}/{to_currency}"
    )
    data = response.json()
    if data["result"] != "success":
        raise CurrencyConversionError(f"{data['error-type']}")
    return Decimal(str(data["conversion_rate"])) * amount

测试的逻辑是, 用无效货币码 CTM 调用 convert_currency ,期望它抛出 CurrencyConversionError 。测试用 patch 替换了 requests.get ,但 mock 配置的是一个 成功 的响应。

问题来了, 如果 mock 真的拦住了请求,函数会正常返回,测试期望的异常不会抛出, pytest.raises 应该失败。但测试实际上 *通过了*。

原因只有一个, mock 根本没拦住调用。真实 API 被请求了, 对无效货币码 CTM 返回了错误, 函数抛出了异常, pytest.raises 就通过了。测试之所以通过, 是因为真实 API 碰巧做了 mock 应该做的事, 跟 mock 一点关系都没有。

这种「假绿」测试特别坑, 它给你的安全感是假的。

怎么证明 mock 真的拦住了

第一反应大概是在 requests.get 之前加一行 print("calling external api") 。但 print 只能证明代码走到了那一行,不能区分是 mock 拦截了调用还是真实网络被触发了。

mock.assert_called_once() 才管用。它检查 mock 对象是不是恰好被调用了一次。如果 patch 没拦住请求, mock 对象压根没被碰过, 断言直接挂掉。

from decimal import Decimal
from unittest.mock import patch, MagicMock
import requests

class CurrencyConversionError(Exception):
    pass

def convert_currency(amount, from_currency, to_currency):
    if from_currency == to_currency:
        return amount
    response = requests.get(
        f"https://example.com/pair/{from_currency}/{to_currency}"
    )
    data = response.json()
    if data["result"] != "success":
        raise CurrencyConversionError(f"{data['error-type']}")
    return Decimal(str(data["conversion_rate"])) * amount

mock_get = patch("__main__.requests.get").start()
mock_get.return_value.json.return_value = {
    "result": "error",
    "error-type": "unknown-code",
}

try:
    convert_currency(Decimal("1.00"), "CAD", "CTM")
    print("ERROR: 应该抛出异常但没有")
except CurrencyConversionError as e:
    print(f"抛出了 CurrencyConversionError: {e}")

mock_get.assert_called_once()
print("assert_called_once 通过:mock 确实拦截了请求")
patch.stopall()
抛出了 CurrencyConversionError: unknown-code
assert_called_once 通过:mock 确实拦截了请求

assert_called_once() 的逻辑很简单, 如果 mock 没被调用过, 说明 patch 没有拦住请求, 你的测试在依赖真实网络。如果 mock 被调用了恰好一次, 说明请求确实走了 mock。在「假绿」测试中加上这行断言后, 作者发现 mock 根本没被调用, 断言直接失败。问题出在, patch 的目标写错了。

patch 的位置决定成败

为什么 mock 没拦住调用?因为 patch 的目标搞错了。前文提到过「patch 的是引用位置」这个规则, 这里展开讲一个更容易踩的坑。

patch 的规则是, patch 的目标是名字 被使用的地方, 而它最初在哪里定义的不重要。

货币转换模块用 import requests 导入 requests 库,然后调用 requests.get(...) 。这时 patch("mymodule.requests.get") 能拦住调用,因为 mymodule.requests 和原始 requests 指向同一个模块对象。

但如果模块改成 from requests import get ,情况就不同了。这时 get 成了模块里的一个 *局部名字*,和 requests.get 不再是同一个引用。你必须 patch mymodule.get 而不是 requests.get 。搞错了位置, mock 就拦截不到。

import requests
from requests import get as direct_get

print(f"requests.get is direct_get: {requests.get is direct_get}")
print("但 patch 替换的是指定命名空间里的引用,不是全局的")
print('patch("module_a.requests.get") 替换 module_a 的 requests 属性上的 get')
print('patch("module_b.get") 替换 module_b 的 get 属性')
print("搞错了命名空间,mock 就拦截不到")
requests.get is direct_get: True
但 patch 替换的是指定命名空间里的引用,不是全局的
patch("module_a.requests.get") 替换 module_a 的 requests 属性上的 get
patch("module_b.get") 替换 module_b 的 get 属性
搞错了命名空间,mock 就拦截不到

这条规则为什么容易踩坑?因为 import requests 的情况下, patch("module.requests.get")patch("requests.get") 恰好都能工作(两个名字指向同一个模块对象)。你以为随便 patch 哪个都行,等到代码改成 from requests import get 的那天, patch("requests.get") 就失效了,而你完全不知道。

mock 的另一个用途, 制造碰撞

前面用 mock 替代了网络请求,但 mock 还有别的用处。文章里还举了一个 CRM(客户关系管理)系统的例子, 联系人用文件存储,每个联系人根据名字生成唯一编码。要测试「两个联系人生成相同编码时抛出 FileExistsError 」, 需要强制让两次调用产生相同的编码。

正常情况下编码生成函数对不同名字会返回不同编码,所以你 patch 它强制固定返回值。

from pathlib import Path
from unittest.mock import patch
import tempfile

def next_code(name):
    """根据名字生成唯一编码(简化版)"""
    return name.lower().replace(" ", "")[:3] + "1"

def create_contact(name, contacts_dir):
    code = next_code(name)
    path = contacts_dir / f"{code}.txt"
    if path.exists():
        raise FileExistsError(f"Contact {code} already exists")
    path.write_text(f"name: {name}")
    return code

with tempfile.TemporaryDirectory() as tmpdir:
    contacts_dir = Path(tmpdir)

    with patch("__main__.next_code", return_value="jd1"):
        code1 = create_contact("Jane Doe", contacts_dir)
        print(f"第一次创建成功,编码: {code1}")

        try:
            create_contact("Bob Smith", contacts_dir)
            print("ERROR: 应该抛出 FileExistsError")
        except FileExistsError as e:
            print(f"正确抛出 FileExistsError: {e}")
第一次创建成功,编码: jd1
正确抛出 FileExistsError: Contact jd1 already exists

patch 强制 next_code 对任何名字都返回 jd1 ,所以两个不同的名字生成了相同的编码,触发了碰撞检测。注意 patch 目标同样是 next_code 被调用 的地方。

比 mock 更好的办法, 显式 override 参数

后来文章作者发现 patch next_code 不是最优方案。给 create_contact 加一个 code 参数, 允许调用者直接指定编码, 就不需要 patch 任何东西了。

from pathlib import Path
import tempfile

def next_code(name):
    return name.lower().replace(" ", "")[:3] + "1"

def create_contact(name, contacts_dir, *, code=None):
            code = code if code is not None else next_code(name)
    path = contacts_dir / f"{code}.txt"
    if path.exists():
        raise FileExistsError(f"Contact {code} already exists")
    path.write_text(f"name: {name}")
    return code

with tempfile.TemporaryDirectory() as tmpdir:
    contacts_dir = Path(tmpdir)

        code1 = create_contact("Jane Doe", contacts_dir)
    print(f"第一次创建成功,编码: {code1}")

        try:
        create_contact("Bob Smith", contacts_dir, code=code1)
        print("ERROR: 应该抛出 FileExistsError")
    except FileExistsError as e:
        print(f"正确抛出 FileExistsError: {e}")
第一次创建成功,编码: jan1
正确抛出 FileExistsError: Contact jan1 already exists

测试通过公共接口( code 参数)控制行为,不需要 patch 任何内部函数。第一次调用捕获自动生成的编码,第二次用同一个编码制造碰撞。

这个做法有代价。给生产代码加一个纯粹为了测试方便的参数, mock 批评者管这叫「测试导致的设计损伤」(test-induced design damage), 意思是你为了测试在生产代码里开了一个本不需要的口子。但作者觉得这里合理, 因为 code 参数同时也是实用功能, 手动指定编码可以用于数据导入或备份恢复。如果参数 为测试存在, 那还是保留 mock 比较好。

注意这跟依赖注入不是一回事。依赖注入(dependency injection)是把函数的 依赖本身 传进来, 测试里换一个假实现。这里传的是依赖 产出的值, 更轻量, 原文称之为「显式 override」。

什么时候别用 mock

mock 的价值在于让你跑快速的单元测试, 不依赖外部服务。判断标准很简单, 测试需要环境变量或外部服务才能跑就是集成测试, 把依赖 mock 掉就变成了单元测试。但有时候答案不是「怎么用好 mock」, 而是别用 mock。

每次 patch("module.requests.get") 都在写一个和导入路径强耦合的测试。你把 import requests 改成 from requests import get ,所有 patch 都会失效。测试在测实现细节,不是在测行为。

文章推荐看 Harry Percival 的 PyCon 演讲「Stop Using Mocks」。核心建议是把外部调用封装到一个适配器类里,写一个内存中的假实现(fake),用依赖注入把假实现传给测试、真实实现传给生产代码。

拿前面的货币转换函数举例, 适配器模式长这样

from decimal import Decimal

class RateProvider:
    """适配器接口: 把汇率 API 的调用封装起来"""
    def get_rate(self, from_currency, to_currency):
        raise NotImplementedError

class FakeRateProvider(RateProvider):
    """测试用的假实现: 不碰网络, 返回固定值"""
    def __init__(self, rate):
        self._rate = rate
    def get_rate(self, from_currency, to_currency):
        return self._rate

def convert_currency(amount, rate_provider):
    rate = rate_provider.get_rate("CAD", "USD")
    return Decimal(str(rate)) * amount

fake = FakeRateProvider(rate="0.75")
result = convert_currency(Decimal("100.00"), fake)
print(f"转换结果: {result}")

转换结果: 75.0000

生产代码传一个真实调用 API 的 RateProvider 子类, 测试传 FakeRateProvider , 函数根本不知道 mock 的存在。

那什么时候 mock 是对的?测试的范围小, 外部依赖就一个, 边界清楚, 这种场景 mock 就很合适。货币转换函数只有一个 requests.get 调用,mock 就是合适的工具。CRM 的 next_code 也是,虽然后来发现显式 override 更好。

先想清楚要不要用 mock。决定用了, 就务必验证它真的生效, 假绿测试比没有测试更危险。