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

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The GitHub Blog
The GitHub Blog
J
Java Code Geeks
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
A
About on SuperTechFans
博客园 - 三生石上(FineUI控件)
罗磊的独立博客
MongoDB | Blog
MongoDB | Blog
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Vercel News
Vercel News
腾讯CDC
博客园 - 聂微东
The Cloudflare Blog
F
Fortinet All Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
Last Week in AI
Last Week in AI
B
Blog

暗无天日

读: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 写文档,却不愿为同事写 - 暗无天日
读:别再手写边界用例了,让 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 的学习曲线是心智转换——从「想一个具体输入」变成「描述一个不变量」。一旦转过来,你会开始到处看到不变量。