Featured image of post 表单只测过 "John Smith":把 246 个会闯祸的取值排进一轮探索性测试

表单只测过 "John Smith":把 246 个会闯祸的取值排进一轮探索性测试

一个表单通常用这样的值验过:一个普通姓名、一个真实存在的日期、一个放得下的数字。然后它遇到真实数据——有人粘贴的名字末尾带空格,有人所在国家…

一个表单通常用这样的值验过:一个普通姓名、一个真实存在的日期、一个放得下的数字。然后它遇到真实数据——有人粘贴的名字末尾带空格,有人所在国家的两位代码恰好是 NO,有人把导出的表格用电子表格软件打开。缺陷报告于是从用户那里一条一条地回来,时间远在代码写完之后。

这些取值既不秘密也不 clever,它们是一张短而稳定的清单。之所以长期没被测到,原因很 mundane:没人记得全,手工敲进去又太麻烦。把这件事工程化的思路很值得借鉴——不是再写一份"坏字符串列表",而是把取值、它通常破坏什么、正确行为应该是什么打包成一个可执行的目录,并让它能在任意输入框里按固定顺序一键送入。

目录里到底有什么

一套已公开的开源目录把 246 个取值组织成 20 个包,每个值都带两个说明字段:breaks(通常破坏什么、为什么)和 expect(正确实现应该怎么做)。缺了 breaks 字段的值在校验时会被直接拒绝——因为这张目录存在的意义正是解释,而不只是罗列。

类别它针对的失效面
空白字符占位置(或声称占位置)却在界面上完全看不见的字符
Unicode 文本看起来无害,却破坏计数、比较与显示
长度炸弹按常见上限构造,以及字符数与字节数之间的落差
魔法值人读成文本、解析器读成布尔值/空值/数字的词
极端数字类型边界、外来记数法、精度耗尽的点
不可能日期不存在的、有歧义的、写法与字段假设不同的日期
导出破坏者通过校验、只在导出文件里出问题的值
本地化标识全球商用产品处理不了的特定语言标识符与格式
文件名与路径文件系统、压缩包或服务端会拒绝、改写、误读的名字
同形对屏幕上相同、存储里不同的成对取值
复杂 emoji基本平面之外的字符与组合序列,破坏计数与截断
编码乱码经历过错误编码且再也回不来的文本
从文档粘贴从字处理软件或网页复制时附带的字符
邮箱与 URL形式上合法但极端的地址,以及只是看起来合法的地址
姓名与身份打破"正常姓名"假设的名字
时区不存在的小时、出现两次的小时、没写时区的时刻
货币与分隔符人与电子表格书写金额的方式,以及精度终点
伪本地化把国际化测试常用技术压缩成可直接粘贴的取值
JSON 载荷解析器与数据库读法不一致的请求体,可改字段名套用到自己的 API
注入检查引号、标签、模板表达式、路径等经典转义检查,被单独标记为敏感

有两个设计细节很实用。过长的值不直接存成字符串,而以"配方"形式保存、发送时才展开,因此一个包能用几行容纳十万字符的长度炸弹;必须保持领先的时间值同样以规则保存,所以时区类始终送入下一次时钟调整,而不是一个已经过去的日期。

二十类取值按"输入 → 校验 → 存储 → 导出"链路分别命中的失效面

先试这八个:取值、后果与正确行为

如果只能抽十分钟,下面八个取值覆盖了最常见、代价也最高的一类遗漏。左侧是取值,右侧是它通常打中的地方,以及正确实现应有的行为。

取值通常破坏什么 / 正确行为
Kowalski 加一个尾随空格浏览器与服务端的比较口径不一致,账号建成了却查不到;应在所有环节统一裁剪,或所有环节统一保留,不能注册一套、登录一套
=1+1电子表格把它当公式,导出文件显示计算结果而不是数据;导出时必须按目标格式转义
noYAML 1.1 与若干配置解析器读成布尔 false,而 NO 同时是挪威的国家代码;类型必须显式声明,不能靠猜
30.02.2026朴素解析器把它滚到 3 月 2 日而不是拒绝;应直接判为非法日期
单个零宽空格字段看起来是空的、实际非空,必填校验被绕过;应在校验前做归一化
128 个字符但占 256 字节界面按字符数放行,存储按字节限长拒绝,或反过来;前后必须统一计数口径
007电子表格把该列当数值后立刻丢掉前导零;导出时应保留文本类型
65 535 个字符常见文本列类型的上限,越界通常直接抛原始数据库错误;服务端应显式校验并给出可读的错误

这里最能说明问题的是 30.02.2026 和 007:它们不是"恶意输入",而是完全正常的真实数据,只是恰好落在实现者的假设之外。也正因为如此,靠"随机 fuzz"未必能稳定命中,靠一张有解释的目录反而更快。

让它变成一轮十分钟的手工测试

目录的交互设计围绕一个目标:不打断测试者的注意力。

  • 一个组合键清空当前行、把当前包的下一个值送进焦点所在的输入框;上一个值、回到包首各有对应组合键,全部可改;
  • 显示面板不抢焦点,光标始终留在被测字段里,值不会被打进面板自己;
  • 不可见字符在面板里画成一个可见标记(如 ␣),同时按四种口径计数:字素簇、码点、字节、UTF-16 单元;
  • 面板显示下一个值以及当前在包里的进度,另有一个检索窗口可跨全部包查找;
  • 发现问题时,一个组合键复制一段可直接粘进缺陷单的报告块:哪个值、来自哪个包版本、以什么形式送入、多大、是否完整到达;值以"别人能重新敲出来"的形式写出,而不是面板上那个占位标记;
  • 图形界面与命令行共用同一套引擎,命令行同样支持剪贴板模式,把值放到剪贴板由测试者自己粘贴。

每个值自带 breaks 与 expect,是这套流程和"一份纯文本清单"最本质的差别:测试者不必先回想"零宽空格会怎样",面板、命令行查看、报告块三处都会直接说明。于是探索性测试从"记得什么就试什么"变成"按固定顺序走一遍",覆盖可复现,也更容易交给新人。

接进自动化:别把清单拷进数组

图形界面适合探索,自动化需要的是同一份数据的机器可读出口。目录工具提供把任意包导出为 JSON、CSV、每行一个值的能力,可选转义或原始形式、可选 base64;诊断信息写到标准错误,因此重定向出来的文件只有值,可以直接当测试数据文件用。

一个典型接法是把"魔法值"包导出成逐行文本,再用参数化测试把它们喂给同一个校验函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import json
import pytest

with open("magic-values.json", encoding="utf-8") as f:
    cases = json.load(f)  # 每项含 value / breaks / expect

@pytest.mark.parametrize("case", cases, ids=lambda c: c["value"])
def test_identifier_roundtrip(case):
    stored = save_identifier(case["value"])
    loaded = load_identifier(stored)
    assert loaded == case["value"], case["breaks"]

把 breaks 直接放进断言失败消息里,缺陷单的描述几乎不用重写;expect 则适合沉淀成团队的用例模板字段。相比在测试文件里硬编码一长串字符串,目录升级时只需重新导出一次。

命令行送入模式还演示了边界处理应有的克制:执行前给几秒让测试者点中目标字段;按住 Ctrl/Alt/Shift/Win 时不输入,目标窗口权限高于自身时不输入,焦点落在按钮、链接、列表项上时不输入,按下 Escape 立即停止,这些情况统一返回一个专门的退出码而不是"猜一下"。自定义包用 TOML 描述,配套的 lint、fmt、new-pack 子命令负责校验格式、规范化与生成骨架——其中校验器目前覆盖 47 条格式规则中的 41 条,剩余几条会明确列出原因。

同一份取值目录同时驱动手工探索、参数化测试与缺陷报告

诚实地说明边界

一份会主动列出自身限制的工具说明,比一份只讲能力的更值得信。这套做法的边界有六条,评估是否引入时都要算进去:

  • 它不判断结果。 只把值送进控件,不读取窗口显示了什么,是否处理得当由人判断;结果标记功能尚未提供,报告块就是唯一的记录。
  • 系统可能改写输入。 某些操作系统的文本替换会把送入的值改成另一个值,工具在检测到相关设置开启且可能影响当前值时会给出提示。
  • 跨平台能力不对称。 向其他窗口送键需要系统授权:某平台需要无障碍权限,另一平台在 Wayland 下需要输入法守护进程且无法确认焦点是否真的落在输入框;缺少条件时,工具明确报错并退回剪贴板模式,而不是静默降级。
  • 自定义包目前可写不可加载。 校验、格式化、生成骨架都可用,但把自己的包目录读进面板与命令行尚未接通;包格式也还没冻结,后续版本可能变更。
  • 246 个值只是一个起点。 它精选的是"漏掉代价最高"的那批,不是任何意义上的完备清单。
  • 它不是攻击工具。 绝大多数取值来自真实用户与真实数据;唯一包含经典转义检查的包被单独标记为敏感,其中没有针对已知漏洞的利用代码,且任何输入都必须由人主动触发。用途是验证自己负责的系统。

国内团队可以这样落地

直接照搬英文包会漏掉本地场景,建议按下面的方式改造成自己的目录:

  1. 把 20 个类别逐一映射到本系统的字段清单,明确每类由哪个页面、哪个接口负责验证,避免"目录有了但没人认领";
  2. 优先覆盖跨系统边界:导入导出、表单到数据库、服务端到报表、开放 API 到下游,这几处是计数口径与类型假设最容易分叉的地方;
  3. 把 breaks / expect 两个字段写进团队用例模板,让每条边界用例都必须回答"破坏什么"和"正确行为是什么";
  4. 把包导出为 CSV 或 JSON,作为参数化测试的唯一数据源,目录更新时重新导出,不在多处维护副本;
  5. 补充中文本地化取值:全角空格、姓名长度与生僻字、含 emoji 的昵称、手机号与工号的前导零、证件号末位的 X、金额的千分位与全角符号、以及中文排序与大小写无关场景;
  6. 把发现的新取值按"值 + 破坏什么 + 在哪见过"的格式贡献回团队目录——一个让某人损失过一下午的输入,值得让所有人只损失一次。

真正需要改变的不是工具,而是测试数据的地位:它不该是随手编的几个"正常值 + 一两个空值",而应当是一份有解释、有顺序、有版本、能同时驱动手工探索和自动化套件的资产。表单没见过的输入,用户迟早会替你送进去。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计