作者在14个月独立开发Windows系统监控过程中,遇到6个测试全通过但实际有bug的静默失败——成功提示出现但文件从未写入、变量空值被忽略、逻辑分支从未执行等,揭示了AI辅助编程时测试覆盖的真实盲区。
崩溃是诚实的,它告诉你出了什么问题。
沉默也是一种声明,它表示一切正常。当这个声明是假的时候,代价极其昂贵,而现在我们正在大规模地生产这种虚假的成功声明。
我独自一人、在下班后的夜晚,开发了一个 Windows 系统监控软件并公开发布了十四个月。所有代码从第一次提交起就放在 GitHub 上,所以这是一份无法篡改的记录。以下是其中的六个故障,它们都没有崩溃,全部通过了我拥有的每一个测试。
Browse the source on GitHub
Install PC Workman from the Microsoft Store Read the project story and technical guides
❤️ If you want to support me, click here :) ❤️

一个风扇曲线编辑器。拖动点,点击应用,绿色消息。
消息出现了。文件从未被写入。两批用户设置了曲线,看到确认,重启一周后发现恢复成了默认值。
零报告,因为用户无法区分"它保存了"和"它说它保存了"。
成功消息不是成功的证据,它只是一个字符串。
temps = psutil.sensors_temperatures() # {} on Windows, always
cpu = temps.get("coretemp", []) # []
if cpu and cpu[0].current > 80: # never true
warn()
psutil.sensors_temperatures() 在 Windows 上返回空字典。不是错误。所以温度监控按计划运行,找不到任何可警告的内容,报告一切正常。持续了数月。
测试断言当温度正常时 warn() 没有被调用。如果你从不断言读取存在,那么空读取与正常读取无法区分。
# Weak: passes when temps is empty, which IS the bug
def test_no_false_alarm():
assert not monitor.check(temps={}).warned
# Stronger: the reading itself is the subject
def test_temperature_source_returns_data():
reading = sensors.read_cpu_temp()
assert reading is not None
assert 0 < reading < 150
如果一个测试在一台该功能被关闭的机器上仍然能通过,那它就不是在测试这个功能。
try:
ctx = build_learning_context()
except:
pass
往上两行,每次调用都有一个 NameError。裸露的 except 吞掉了它。没有崩溃,没有日志。学习引擎运行了四个月并产生了正确的数字,而消费它们的那一层从未收到任何东西。
一个裸的 except: pass 并不是在处理错误,它是在删除错误发生过的证据。
去 grep 你的项目中 except: 后跟 pass 的地方。我等你。
except Exception as e:
log_event("learning_context_failed", repr(e)) # never silent
ctx = None
一行代码。替代方案的价值是四个月。
仪表盘上的 TURBO 开关。可点击。有动画。它写入了一个代码库中没有任何东西读取的标志。
我通过直接调用函数来测试这个功能。我通过观察来测试按钮。
一个功能的两个半部分可能各自都正确,但合在一起仍然不是一个功能。
打包的 Windows 应用位于用户无法右键进入的文件夹中,所以应用为他们创建一个桌面快捷方式。它必须指向一个由包族名称加上清单中的应用程序 ID 组成的标识符。
manifest: Application Id="App"
shortcut code: ...PCWorkman_4hekbcs2ddfbc!PCWorkmanHCK
每个使用该功能的商店用户都得到了一个启动什么都不了的快捷方式。没有错误。零 bug 报告,因为没有人会为一个什么都不做的快捷方式提交工单。他们双击两次,耸耸肩,再也不会用它。
必须在两个文件之间保持一致的标识符迟早会不一致。测试一致性,而不是测试任何一方。
一个具有签名、误植域名和伪装检测的进程检查引擎。它正确地捕获了冒充 svchost.exe 的 svch0st.exe。
它也标记了 spoolsv.exe,带有来自正确 System32 路径的有效 Microsoft 签名。
原因:进程库携带了一个注释,意思是"重量级,注意资源使用"。那个注释抬高了安全裁决。一个字段中承载了两种不同的truth。
当一个字段承载两种truth时,其中一种最终会回答一个它从未被问过的问题。
静默失败是成功的虚假声明。不是输出的缺失,而是一个碰巧令人安心的错误输出。
我在每篇文章中都说明了与 AI 助手合作。正常情况下那意味着一个对话:
提问、仔细阅读、质疑其中三分之一、保留存活下来的部分。
然后到了一个发布周。商店提交,跨 42 个文件的版本号递增,一个构建,一个包,还有在方向盘后面度过的日班。
我开始接受得越来越多,阅读得越来越少。在那一周里:
合理的数据被写入数据库。known-process 库增加了三十五条条目。
供应商字段看起来完全合理。bash.exe 被归因于"Git Development Community"。真正的 Authenticode 签名者是一个人的名字,而引擎会比较预期供应商与实际签名,所以不匹配会引发警告。
结果:三个只是未被识别的进程变成了被标记为可疑的。自信、合理,但比什么都不写更糟糕。
一条通过测试却在生产环境失败的 regex。
# Passed every unit test. Never matched in the running app.
pos = text_widget.search(r'\[-> [^\]]+\]', idx, regexp=True)
单元测试使用的是 Python 的 re。控件的 search 由 Tcl 求值,而 Tcl 的括号表达式对 [^\]] 的处理方式不同。两个引擎,一个字符串,没有错误消息。每条链接都渲染成了纯文本。
修复:搜索一个字面前缀,用写出该模式的引擎来解析。
pos = text_widget.search('[-> ', idx) # literal, engine-agnostic
m = re.match(r'\[-> ([^\]]+)\]', line_text) # Python parses Python
一条 git reset --hard 抹掉了一天的未提交工作。只因为一小时前有一个完整备份才可恢复。
一个清理脚本吃掉了 38 个逗号。一次跨 15 个 HTML 文件的标点处理删除了 38 个闭合标签后面的逗号。"Driver conflicts, leftover GPU packages" 变成了 "Driver conflictsleftover GPU packages"。
没有崩溃。每个页面都完美渲染。
这些都没有抛出异常。它们都产生了看起来正确的输出。
这不是反对这种方式工作的论点。项目因此更快了。这是一个关于审查必须发生在哪里的论点。
生成的代码在结构上是流利的:它能编译、读起来很好、使用了正确的函数名。流利不等于正确,而流利正是让差异变得不可见的原因。
验证事实,绝不接受看似合理的。如果一个值可以从系统读取,就读取它。一个看似合理的事实比一个缺失的事实更危险,因为缺失的事实会被检查。
验证事实,绝不接受看似合理的。如果一个值可以从系统读取,就读取它。一个看似合理的事实比一个缺失的事实更危险,因为缺失的事实会被检查。
问哪个引擎实际运行这个。在信任一个绿色测试之前,问它是否 exercise 了用户实际使用的同一个运行时。
问哪个引擎实际运行这个。在信任一个绿色测试之前,问它是否 exercise 了用户实际使用的同一个运行时。
绝不让破坏性命令未经阅读就通过。任何带有 --hard、--force、rm、DROP 或 reset 的命令都要逐字符阅读。先备份。
绝不让破坏性命令未经阅读就通过。任何带有 --hard、--force、rm、DROP 或 reset 的命令都要逐字符阅读。先备份。
检查输出,而不是退出码。逗号脚本"成功了"。供应商条目"成功了"。上面所有六个 bug都"成功了"。
检查输出,而不是退出码。逗号脚本"成功了"。供应商条目"成功了"。上面所有六个 bug都"成功了"。
当天写一个棘轮。修复只完成了一半工作。另一半是一个测试,如果 bug 回来了它会让构建失败。它们只会往一个方向转动。这里的每个 bug 现在都有一个这样的测试。
点击那个东西。在一个将一个模块拆成七个的重构之后,96 个测试保持绿色,而每个侧边栏页面都静默地回退到了仪表盘。没有一个测试构建了真实的窗口。五分钟的人工点击捕获了整个测试套件无法捕获的东西。
在第一个分歧处埋点,而不是在末尾处测量损害。一个重放 bug 花了我三天时间,测量两次运行最终相差多远。那个数字告诉你损害的大小,而不是别的。
日志记录第一次它们停止一致的 tick,把几个夜晚变成了几分钟。
信任探针,而不是名字。通过检查路径是否包含 WindowsApps 来检测只读安装文件夹是对世界的一个猜测。写入探针是关于它的事实。
我独自开发。没有审查者,没有人可以问"你检查过它真的保存了吗"
这些 bug 存活下来不是因为它们很微妙。有几个是显而易见的。它们存活下来是因为只有一个可能抓住它们的人,而那个人已经认定这个功能是工作的。
你不会为一个你已经相信工作的功能写测试。
这不是技术问题。这是成为唯一见证者的问题。
我 22 岁,在波兰,技术学校毕业后自学成才,在这个项目之前有十二个项目死掉了。构建大部分代码的笔记本电脑是 2014 年的,温度达到 94 度。
白天的工作先是仓库,然后是塑料焊接,现在是出租车。你不会在十二小时轮班后的午夜做仔细的审查。你做那个感觉已经完成的事情。
这篇文章前半部分的每一样东西,都是六个月后"感觉已完成"的样子。
有三件事有帮助,但都不是靠自律:在公开场合写作(你描述一个功能的方式和它实际做什么之间的差异就在那里)、向不欠你任何东西的人交付(一个测试者拒绝接受"它在我机器上能工作"关于一个无法关闭的控制台,他是对的)、为六个月后的你保留一份日志。
我们在生成看起来正确的代码方面越来越擅长,越来越快地生成它。两者都不能让代码更有可能做你打算做的事。
不要把结果当作行动的证明。不要从你的代码、你的工具、任何为你生成文本的东西那里接受这一点,也不要在午夜时从你自己那里接受。
寻找收据。
我开发 PC Workman,一款带有完全离线助手的高级 Windows 系统监控工具。331 个自动化测试和一份公开的上述所有内容清单。