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

推荐订阅源

Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
U
Unit 42
H
Help Net Security
博客园_首页
雷峰网
雷峰网
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园 - 【当耐特】
Recent Announcements
Recent Announcements
Recorded Future
Recorded Future
aimingoo的专栏
aimingoo的专栏
爱范儿
爱范儿
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Cloudflare Blog
S
Security @ Cisco Blogs
M
MIT News - Artificial intelligence
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
Spread Privacy
Spread Privacy
The GitHub Blog
The GitHub Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
K
Kaspersky official blog
MyScale Blog
MyScale Blog
阮一峰的网络日志
阮一峰的网络日志
C
CERT Recently Published Vulnerability Notes
V
Vulnerabilities – Threatpost
WordPress大学
WordPress大学
C
Cisco Blogs
G
Google Developers Blog
N
News and Events Feed by Topic
P
Palo Alto Networks Blog
Apple Machine Learning Research
Apple Machine Learning Research
Schneier on Security
Schneier on Security
博客园 - 聂微东
Security Latest
Security Latest
Security Archives - TechRepublic
Security Archives - TechRepublic
O
OpenAI News
云风的 BLOG
云风的 BLOG
IT之家
IT之家
PCI Perspectives
PCI Perspectives
Microsoft Security Blog
Microsoft Security Blog
NISL@THU
NISL@THU
小众软件
小众软件
Scott Helme
Scott Helme
大猫的无限游戏
大猫的无限游戏
L
LINUX DO - 最新话题
Microsoft Azure Blog
Microsoft Azure Blog
Simon Willison's Weblog
Simon Willison's Weblog
N
News and Events Feed by Topic
F
Fortinet All Blogs
The Last Watchdog
The Last Watchdog

暗无天日

读: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 - 暗无天日
读:别再手写边界用例了,让 Hypothesis 自动找 bug - 暗无天日
darksun,Claude Code · 2026-05-31 · via 暗无天日

目录

  • Property-based testing 是什么
    • 三个最常见的适用属性
    • Round-trip(往返不变性)
    • Idempotency(幂等性)
    • Monotonicity(单调性)
  • 安装 Hypothesis
  • Strategies,描述你的输入空间
  • Shrinking,找到最小的失败输入
  • 集成 pytest
  • 什么时候该用
    • 用传统测试来,
    • 用 Hypothesis 来,
    • 三行护身符
  • 结语

原文是三年前的一段经历。作者为一个URL标准化器手写了47个边界用例,每个用例都是同样的套路,想一个奇怪的输入,写测试,确认代码能处理。

然后 Hypothesis 对同一个函数生成了10000个随机输入,30秒内就找到了一个他从未想过要写测试的 bug。当输入全是空白字符时,标准化器静默返回空字符串。

这47个测试用的全是「看起来像URL」的输入。Hypothesis 能找到空白字符问题,恰恰是因为它不做任何假设,它试图在约束的边界上生成输入来破坏你的函数。这就是核心转变,从描述「例子」变成描述「属性」。

Property-based testing 是什么

传统测试会说,给定这个特定输入,我期待这个特定输出。Property-based testing 则换了问法,不管输入是什么,你的函数得守住哪些底线?那些底线就叫不变量(invariant),比如「编码再解码应该返回原始数据」。你描述一条不变量,Hypothesis 自动生成各种输入来检查它是否真的守得住。

用 Hypothesis 的话,你只写那个不变量,它会自动生成成百上千个输入来尝试证伪它。如果找到失败的用例,它会自动收缩到仍然失败的最简输入。所以你得到的是一个小巧可调试的失败案例,而不是「在5000个字符的字符串上失败了」。

看代码对比。先看传统测试,

def test_round_trip_specific():
    assert decode(encode("hello world")) == "hello world"

再看 property-based 版本,

from hypothesis import given, strategies as st

@given(st.text())
def test_round_trip_any_string(s):
    assert decode(encode(s)) == s

第二个测试会运行几百次,每次用不同的输入。如果 encode/decode 在 unicode 字符、空字符串、null 字节或恰好256个字符长的字符串上有 bug,Hypothesis 就会找到它。

三个最常见的适用属性

几乎所有函数都有一些「应该始终成立」的不变量。以下三个是最常见的切入点,

Round-trip(往返不变性)

把数据变形再变回来(编码解码、序列化反序列化、压缩解压),应该得到原始数据。

Idempotency(幂等性)

对同一个数据执行两次操作,结果应该和执行一次一样。URL 标准化、数据清洗、格式化都属于这类。

Monotonicity(单调性)

某些操作的结果不会超出输入的范围。比如过滤后的列表不会比原列表长,排序后的结果不会引入新元素。这个性质不如前两个直观,但理解之后你会发现很多场景都适用。

安装 Hypothesis

安装不需要注册账号、不需要 API key、不需要配置任何服务,

如果同时装了 pytest(Hypothesis 测试本来就用 pytest 跑),

pip install hypothesis pytest

Hypothesis 是纯 Python 库,输入在本地生成,找到的失败用例存在本地。property-based testing 的全部威力,零外部依赖。

Strategies,描述你的输入空间

st 模块用来描述输入空间。几个常用策略,

from hypothesis import strategies as st

st.integers()                       st.integers(min_value=0)            st.floats(allow_nan=False)          st.text()                           st.text(alphabet=st.characters(allowed_categories=('Lu', 'Ll', 'Nd')))  st.binary()                         st.booleans()                       
st.lists(st.integers())                             st.lists(st.text(), min_size=1, max_size=50)        st.dictionaries(st.text(), st.integers())           st.tuples(st.integers(), st.text())                 
st.one_of(st.text(), st.none())                     st.builds(MyDataClass, name=st.text(), age=st.integers(min_value=0, max_value=150))

builds 策略算是个亮点。它为数据类或 Pydantic 模型的每个字段分别生成输入,自动构造测试对象,

from hypothesis import given, strategies as st
from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

@given(st.builds(User, name=st.text(min_size=1), age=st.integers(min_value=0, max_value=150)))
def test_user_serialization_round_trip(user):
    assert User.model_validate_json(user.model_dump_json()) == user

Shrinking,找到最小的失败输入

当 Hypothesis 找到一个失败输入时,它不会直接报告原始输入。它会自动收缩,尝试越来越简单的输入,直到找到仍然触发失败的最简用例。

没有 shrinking 的话,失败报告可能是「在3000个字符的字符串上失败」。有了 shrinking,它变成,

Falsifying example: test_normalize_idempotent(
    url='',
)

不需要做任何额外配置,所有内置策略都自带 shrinking。用 st.composite 和标准组合器构建的自定义策略也能自动收缩。

集成 pytest

Hypothesis 测试看起来就是加了装饰器的 pytest 测试,集成完全无缝,

import pytest
from hypothesis import given, settings, strategies as st
from myapp.normalizer import normalize_url

def test_normalize_removes_trailing_slash():
    assert normalize_url("https://example.com/") == "https://example.com"

@given(st.text())
def test_normalize_idempotent(url):
    assert normalize_url(normalize_url(url)) == normalize_url(url)

@given(st.text(min_size=1))
def test_normalize_never_empty_on_nonempty_input(url):
    assert normalize_url(url) != ""

按常规方式运行 pytest 就行,Hypothesis 测试会自动被发现和执行。

默认情况下,每个测试运行约100个随机示例。你也可以通过 @settings 调整,

from hypothesis import given, settings, strategies as st

@settings(max_examples=1000)   @given(st.text())
def test_important_property(s):
    ...

@settings(max_examples=50)     @given(st.lists(st.integers()))
def test_basic_property(items):
    ...

什么时候该用

Property-based testing 不是传统测试的替代品,两者结合比单独用任何一个都强。

用传统测试来,

  • 文档化预期行为(这个特定输入应该输出什么)
  • 测试已知的边界用例(你已经想到的那些)
  • 回归测试(那个 bug 有一个特定输入,把它记下来)

用 Hypothesis 来,

  • 处理或转换数据的函数(解析器、标准化器、序列化器)
  • 有「不管输入是什么,结果都得满足某条规则」的函数(排序稳定性、往返正确性)
  • 系统边界处的函数,输入空间很大或不可预测
  • 找到你没想到要找的 bug

三行护身符

给你的解析器和数据处理函数加上 test_does_not_crash

@given(st.text())
def test_does_not_crash(s):
    """函数应该能处理任何输入而不抛出意外的异常。"""
    try:
        result = my_function(s)
                assert isinstance(result, (str, type(None)))
    except ValueError:
        pass      except Exception as e:
                raise AssertionError(f"Unexpected exception for input {s!r}: {e}") from e

这行代码不断言正确性,它只断言函数不会在任意输入上炸掉。那些「从来只用合法数据调用」的函数,用这个模式一测就能翻出一堆你没想到的 bug。

结语

Property-based testing 的学习曲线是心智转换——从「想一个具体输入」变成「描述一个不变量」。一旦转过来,你会开始到处看到不变量。