tests-and-ai-coding-tools.html
< BACK 破旧的书桌配上笔记本、打开显示代码的笔记本电脑,以及伦敦冷酷窗光下的一杯茶

我重新开始写测试的原因(AI逼的)

早在2022年初,我做了一个无声的决定:我停止为通过Seahawk进来的大多数WordPress和Node项目写单元测试。没有大张旗鼓。没有博客文章。我就这样……停止了。我当时认为这个理由很充分——我们每月交付15到20个客户网站,我有三个其他开发者轮换,而我写的测试就像没人读的文档。手动QA捕获了真正的bug。测试不过是场表演。

快进到2023年底。GitHub Copilot在我的编辑器里已经有了大约八个月。我也开始在一些greenfield项目上使用Cursor。速度真的很快。但有些事开始发生。bug出现在我没接触过的地方。看起来正确的逻辑在我从未想过检查的边界情况下是错的。最糟糕的是——AI根本不知道它错了。它用同样自信的缩进写出了破损的代码。Cursor on the side for anything greenfield. The speed was genuinely remarkable. But something started happening. Bugs were appearing in places I hadn't touched. Logic that looked correct was wrong in edge cases I'd never thought to check. And the worst part — the AI had no idea it was wrong. It wrote the broken code with the same confident indentation it always does.

就是那时我重新拿起了测试。

---

我放弃测试的时期(以及为什么当时有意义)

老实说?对于某些特定项目,跳过测试是正确的决定。如果你在WordPress中构建一个五页的宣传册网站,为联系表单插件编写PHPUnit测试纯粹是浪费时间。我坚持这个立场。was the right call. If you're building a five-page brochure site in WordPress, writing PHPUnit tests for a contact form plugin is theatre. I stand by that.

Seahawk长期以来的主要业务正是这类工作——高产量、相对低复杂度、范围明确。客户交给你一个Figma文件,你构建它,你进行QA测试,你上线。反馈周期很短。如果出了问题,你会在几小时内发现。在这种情境下编写测试,就像给便签纸贴膜一样。

但我过度推广了那个经验。我开始把所有项目都当成宣传册网站来对待。即使是那些有自定义WooCommerce结账流程的项目。即使是我们在2023年初为法兰克福一个客户构建的金融科技仪表板——完整的自定义REST API、JWT认证、三个不同的用户权限级别。没有测试。只有"仔细的手动QA"。这太傲慢了,也为此付出了代价。all projects like brochure sites. Even the ones with custom WooCommerce checkout flows. Even the fintech dashboard we built in early 2023 for a client in Frankfurt — full custom REST API, JWT auth, three different user permission tiers. No tests. Just "careful manual QA." That was arrogant, and it bit us.

法兰克福项目上线时存在一个权限漏洞,让编辑级用户在特定的过滤条件组合下能够查询管理员级的数据。我们直到他们的内部团队在上线后六周进行安全审计才发现。很尴尬。虽然可以修复。但这种问题本来可以被基础集成测试在我们提交拉取请求前就标出来。

---

AI编码工具实际上改变了什么

人们在谈论Copilot、Cursor或任何本月最火的模型时,往往忽略的是:代码看起来是对的。这就是问题所在。looks right. That's the problem.

当初级开发者写出有bug的代码时,你通常能看到其中的不确定性。奇怪的变量名、一条注释说"// not sure about this"、一个明显被复制粘贴两次的函数。代码会暴露自己的脆弱性。AI代码不会。它在风格上是一致的,命名得当,结构看起来很有意图。自信完全是表面现象。// not sure about this, a function that's clearly copy-pasted twice. The code telegraphs its own fragility. AI code doesn't. It's stylistically consistent, well-named, and structured in a way that reads as intentional. The confidence is entirely cosmetic.

斯坦福大学人机交互小组的研究表明,使用AI助手的开发者往往在第一眼就过度信任生成的代码。这与我的实际经历相符。我会扫一眼Copilot写的40行函数,想"嗯,基本就是我会写的样子",然后继续。有时候没问题。有时候它在不知不觉中误解了我的真实需求。 have flagged that developers using AI assistants tend to over-trust generated code on first read. That tracks with my own experience. I would glance at a 40-line function Copilot had written, think "yeah, that's basically what I'd have written," and move on. Sometimes it was fine. Sometimes it had silently misunderstood what I actually needed.

我一直遇到的特定失败模式是:围绕边界情况的条件逻辑,这是AI没有理由预期的。它会写一个在正常路径上完美运行的函数,然后在null输入、空数组或非标准日期格式上悄悄失败。这些是我自己写代码时三十秒就能想到的问题,因为我会边写边思考。conditional logic around edge cases the AI had no reason to anticipate. It would write a function that handled the happy path perfectly and then quietly fail on null inputs, empty arrays, or non-standard date formats. Things that would have taken me thirty seconds to think about if I'd written the code myself, because I'd have been thinking as I typed.

速度陷阱

这里存在一个真实的生产力陷阱。AI 让你变快。快速感觉像是好的。你开始更快地发布,你开始更不仔细地审查,因为速度感觉像是质量的证据。但它不是。当你在提示语言模型时,速度和正确性没有相关性。

去年九月,我在一个客户项目中投入了大约 40% 更多的功能,这是没有 AI 助手我无法做到的。这个项目在上线后也出现了比我在两年内发布的任何东西都更多的错误。不是灾难性的错误。但很烦人。那种会侵蚀客户信任的错误。

---

为什么测试现在的工作方式不同(AI 参与其中)

当我回到测试时,我没有回到旧的工作流程。先写测试,然后实现,然后进行 AI 辅助的代码审查——这是我现在已经确定下来的循环。

有趣的是,AI 在编写测试方面实际上非常出色,在编写应用程序逻辑时并不总是这样出色。给 Copilot 一个定义良好的函数签名,要求它生成测试套件,它会产生边界情况覆盖,我手动编写需要花费二十分钟。当任务具体是"找出这可能如何破坏"时,它对不顺利的路径想象得很好。AI is actually excellent at writing tests, in a way it isn't always excellent at writing application logic. Give Copilot a well-defined function signature and ask it to generate a test suite and it'll produce edge-case coverage I'd have taken twenty minutes to write manually. It imagines unhappy paths well when the task is specifically "find ways this can break."

所以我有点颠倒了这个东西。我编写测试规范。AI 填充测试用例。然后 AI 编写实现。然后我通过这些测试的角度来阅读实现,而不仅仅是直接阅读代码。through the lens of those tests, rather than just reading the code cold.

这比纯粹的凭直觉编码要慢。但它比旧的手动编写所有内容(包括测试)的工作流程更快。自法兰克福以来,它没有发布过任何权限错误。

我实际在使用的工具

  • [Vitest](https://vitest.dev) 用于任何 JavaScript 或 TypeScript 项目。去年完全替代了 Jest——配置更清晰,监视模式也很快。 for anything JavaScript or TypeScript. Replaced Jest for me entirely last year — the config is saner and the watch mode is quick.
  • PHPUnit 仍然是首选,用于 WordPress 和自定义 PHP 工作。没有什么能替代它。 still, for WordPress and custom PHP work. Nothing has replaced it.
  • Cursor 的"测试此函数"快捷方式——真的是我用过的任何编辑器中最有用的单个功能之一。 — genuinely one of the most useful single features in any editor I've used.
  • GitHub Actions 用于 CI。每次推送到 main 时都会运行测试。大多数项目花费约 90 秒。 for CI. Tests run on every push to main. Takes about 90 seconds on most projects.

---

反对测试的论点(理性阐述)

我想认真对待这个观点,因为我曾持有这个立场近两年。

真正的论点不是"测试没用"。而是"测试有成本,许多项目都不足以承担这个成本"。编写和维护测试套件需要时间。对于生命周期短的项目——活动微网站、营销落地页、黑客马拉松原型——这笔时间投资没有任何回报。项目会在测试能帮助你之前就已经结束了。

还有一个更微妙的问题:糟糕的测试比没有测试更糟糕。一个测试套件通过是因为测试本身就是同义反复(你基本上是在测试你的函数是否返回了你告诉它返回的东西),这会给你虚假的信心。我在代理机构看过这种情况。开发者写的测试总是通过,因为没有人质疑他们实际在验证什么。bad tests are worse than no tests. A test suite that passes because the tests are tautological (you're essentially testing that your function returns what you told it to return) gives you false confidence. I've seen this at agencies. Developers writing tests that always pass because nobody challenged what they were actually verifying.

Martin Fowler 已经写过很好的相关文章——覆盖率百分比不是测试质量的度量。90% 的覆盖率数字可能掩盖了一个完全空心的测试套件。 — coverage percentages are not a measure of test quality. A 90% coverage number can mask a completely hollow suite.

所以:不要测试所有东西。不要因为感觉专业就测试。测试是因为你已经识别出了承重的逻辑,而破坏它会很昂贵。

---

我现在测试的内容(以及我不测试的)

这是我在过去八九个月里得出的实际决定:

我测试:

  1. 任何处理金钱、权限或数据转换的函数
  2. 任何不是直接 CRUD 传递的 API 端点
  3. 客户在书面上指定了确切行为的自定义业务逻辑
  4. 任何我没有逐行完整阅读的 AI 生成代码

我不测试:

  • UI 渲染(快照测试在我的九年职业生涯中从未救过我一次。一次都没有。)
  • 第三方 API 包装器,其中外部行为不在我的控制范围内
  • 运行一次就被删除的一次性脚本
  • 标准的 WordPress hooks,除非它们做的是不同寻常的事情

就这样。没有宏大的哲学。只是一个基于我踩过的坑的列表。

---

实际上对我有效的工作流程

由于一些在 Slack 社区中认识的人提过问题,这是我的实际流程:

  1. 在文件顶部写一个简短的规格注释——这个模块做什么,不做什么,我已经知道的边界情况。
  2. 在编写任何实现之前,让 Cursor 根据那个注释生成测试用例。
  3. 检查那些测试用例。删除愚蠢的。添加AI遗漏的任何用例。
  4. 让Copilot或Cursor写实现代码。
  5. 运行测试。它们会失败。修复实现(不是测试)。
  6. 推送前读一遍diff——AI辅助代码仍然需要人工审查。

第6步是不可协商的。过去四个月我仅通过在推送前慢速阅读diff就发现了三个真实存在的严重bug。没什么聪明的。就是阅读。

Kent Beck最初对TDD的定义从来不是100%覆盖率或完美的方法论。而是建立一个足够快的反馈循环来在错误复合之前捕捉它们。这个理念——快速反馈循环——现在比2003年时更加相关。因为AI犯错的速度比我聘请过的任何开发者都快。 was never about 100% coverage or perfect methodology. It was about building a feedback loop fast enough to catch mistakes before they compound. That idea — fast feedback loops — is more relevant now than it was in 2003. Because the AI makes mistakes faster than any developer I've ever hired.

---

FAQ

这会减慢你的交付速度吗?

在复杂项目中大约慢10到15%。在简单项目中,也许根本不会——AI生成测试的速度很快,所以开销最小。对于那些bug在发布后修复会花真金白银的项目(大多数真实项目都符合),这15%绝对值得。

TypeScript呢?强类型不是替代了很多测试吗?

部分是的。TypeScript在编译时捕获了一大类错误,这些错误你过去需要通过测试才能发现。但类型不会测试业务逻辑。它们不会验证你的折扣计算函数是否为批发客户应用了正确的规则。这部分还是你的责任。

初级开发者在不写测试的情况下应该使用AI编码工具吗?

不应该。我的看法很明确。初级开发者使用Copilot但不写测试,本质上就像在自动驾驶模式下飞飞机,却不理解自动驾驶怎么工作,也不知道如何手动着陆。AI会生成看起来很高级的代码,初级开发者不知道哪些部分不可信,最后你会遇到生产事故。至少测试给了他们一种机制来验证他们接受的输出。

说实话,你为什么一开始就不测试了?

部分是倦怠。还有一段时间每个项目都真的很简单,测试真的没什么用。错误在于没有注意到项目复杂度何时增加,并做出相应调整。真正的教训不是"总是测试"或"永远不测试",而是知道一个给定项目属于哪一类。

---

写测试过去不像是保护。感觉像是打杂。AI改变了这一点。不是因为AI不好——它让我的速度确实快了很多——而是因为它引入了一类新的、自信的、格式良好的、看起来很靠谱的错误,我没办法像过去一样通过读代码来发现。这些测试不是为了AI。是为了我。在接受模型给我的代码之前,强迫我思考我实际需要代码做什么。

我希望两年前就这样框架化了。

< BACK