用写代码来实现想法,用写测试来维持确定性

在没有 AI 编程工具之前,开发一个软件的过程往往比较耗时。

开发者要先在脑子里理清数据结构,一行行写出业务逻辑,再手动把界面画出来。

那个时候,编写具体的代码占用了大部分精力,每增加一个按钮、联调一个接口,都需要反复调试好一阵子。

这两年大语言模型普及之后,写代码的方式发生了很大变化。

很多人发现,只要在对话框里用自然语言描述需求,几分钟就能生成一套包含前端、后端和数据库的基础代码。

不少文科背景或者没有受过系统计算机训练的朋友,也开始凭借这种 Vibe Coding 的方式,做出了自己的第一个产品。

但在迈过最初的尝鲜阶段之后,很多人都会遇到一段相似的瓶颈期。

当软件里的功能从两三个增加到十几个,改动代码开始变得容易引发意外。

有时你只是让模型微调一下主页的排版,结果结算页面的按钮突然点不动了;或者让它修一下用户名不能为空的校验规则,原本正常的登录弹窗突然无法弹出。

最麻烦的是,你很难第一时间发现它是在哪次改动中悄悄失效的。

很多刚接触开发的朋友,最直接的办法就是在浏览器里手动把所有功能挨个点一遍。

页面少的时候还能应付,等页面和分支多起来之后,每次改完代码都耗费大量时间逐项核对,上线前心里也不太踏实。

这时往往会产生一个疑问:为什么计算机执行代码不能做到完全不出错?为什么自己或者 AI 写出来的程序,总是容易冒出各种 Bug?

1. 为什么软件会有 Bug

要理解软件为什么会出问题,可以看一下这个词最早的历史。

一九四七年,计算机科学家格蕾丝·霍珀在哈佛大学调试一台巨型继电器计算机时,机器突然停止了运转。

经过排查,工作人员在继电器的两个触点之间,夹出了一只被电死的飞蛾。

霍珀把这只飞蛾用胶带粘在实验日志里,写下了找到第一个真实虫子的记录,排查程序故障从此被称为 Debug。

但今天我们在代码里遇到的 Bug,已经和虫子没有关系了。

它源于一个很客观的矛盾:现实世界充满了各种偶发细节,而计算机逻辑是严格按规则执行的。

代码本质上是人类对现实世界规则的一种简化抽象。

比如在现实里去咖啡店买一杯咖啡,这个流程看似很简单,但真实环境包含很多偶然情况:顾客付钱的瞬间手机可能刚进电梯断网了两秒,微信钱包里的余额可能刚好差了一分钱,排队的人可能由于网络延迟连点了三次支付,收件人的名字里可能带了生僻字或者特殊符号。

人类在现实中遇到这些情况,会凭借常识灵活处理;但计算机不同,它只认代码里写明了的逻辑规则。

不管是人类工程师还是大语言模型,在最开始抽象这个点单流程时,都很难一次性把所有偶发情况全部提前预料到。

只要遇到一种代码里没定义规则的情况,程序不知道该往哪走,就会直接报错、白屏或者把数据算错。

代码没有完全覆盖现实规则留下的这些漏洞,就是所谓的 Bug。

2. 自动化测试与 TDD

既然人的思维很难一次性把所有边界情况想周全,软件工程在过去几十年里摸索出的一套标准办法,就是做自动化测试。

测试其实就是专门写几段辅助代码,作为持续运行的校验机制。

每当我们改动了一行业务代码,校验脚本就会自动把整个系统重新跑一遍,检查计算结果是否准确、对应页面有没有正常响应。

在软件开发的历史上,有一套很经典的实践方法,叫测试驱动开发,也就是常说的 TDD。

过去很多人习惯先写功能代码,写完之后再去手动验证;但测试驱动开发的思路正好反了过来:在动笔写任何具体功能之前,先把测试规则定好。

比如要开发一个最基础的会员注册功能,工程师会先写下几道具体的断言:输入格式正确的邮箱必须顺利注册成功,输入重复的邮箱必须提示已被占用,密码长度不足六位的不准通过并给出提示。

把这几道规则转化成测试代码之后,再去写具体的业务逻辑。

让业务代码在后台反复跑这套规则,直到所有断言全部通过。

很多运行了多年、代码错综复杂的大型系统,之所以能平稳地做底层重构,正是因为手里握着这套严密的测试用例。

当这种做法放到今天的 AI 编程里时,契合度非常高。

大语言模型在生成代码时容易产生随机偏差,但如果在一开始就给它设定好客观严谨的测试规则,大模型就会把跑通测试当成明确目标。

它会自己写代码、自己跑测试、根据报错信息一遍遍修正,直到全部跑通。

有自动化测试托底,AI 交付出来的代码稳健性会高出很多。

3. 单元测试、集成测试与端到端测试

为了把测试覆盖得更周全,工程上通常会把测试分成几个不同的观察视角。

最微观的视角,是去检查每一个独立的计算逻辑,这叫单元测试。

比如系统里写了一个计算会员打折的独立函数,单元测试就会自动输入一百块钱,检查算出来的结果是不是八十块。

这种测试不需要启动前端界面,也不需要连接数据库,几毫秒就能把成百上千个公式跑完,执行成本很低。

但单独的计算公式都对,并不代表它们拼在一起能正常运转。

把打折公式、用户数据库和支付系统接在一起时,数据格式能不能对齐,接口调用顺不顺畅,这就是集成测试在做的事情。

而最接近真实用户体验的,是端到端测试,也就是大家常说的 E2E。

端到端测试做的事情,是直接模拟真实用户的操作链路。

它会自动打开浏览器,在输入框里打字,用鼠标点击各个按钮,检查最后屏幕上有没有渲染出付款成功的界面。

整条交互链路能完整走通,系统才算真正可用。

在这个过程中,很多人会经常关注一个指标,叫测试覆盖率。

它衡量的是测试跑完时,系统里有多少比例的代码行数被实际执行到了。

但这个数字有时会带来虚假的安全感。

如果测试代码只是调用了一遍功能,中途没有做细致的结果断言,哪怕覆盖率达到了百分之九十,实际运行依然可能出问题。

更何况现实业务里充满了各种条件分支,如果只验证了其中一种走通的路径,那些没被覆盖到的异常情况,线上依然可能报错。

4. 端到端测试为什么难维护

在所有的测试类型里,端到端测试因为直接模拟真实交互,往往被视为很关键的一道防线。

但过去很多独立开发者和小团队,在日常开发中却很少写这种测试。

主要原因是传统的端到端测试维护成本非常高。

在过去很长一段时间里,要在浏览器里做自动化操作,行业里最常用的工具是 Playwright 和 Cypress。

这些工具的运作逻辑,是依靠预先写好的脚本去控制浏览器。

开发者需要提前把指令写得非常精确:打开网页后,去底层代码里找到带有特定属性或路径的按钮,模拟点击一次,再去检查屏幕上的文字。

这种定位按钮的代码标记,在技术上统称为选择器。

问题在于,前端网页的样式和代码结构变化非常频繁。

特别是在用 AI 调整页面时,大模型为了改一个排版或者换个配色,经常会顺手把底层的标签和类名重写一遍。

在人眼看来,屏幕上的按钮依然摆在原地;但脚本是严格按照旧名称去寻找的,名称一旦对不上,脚本找不到目标就会报错中断。

改业务功能往往只花了几分钟,但为了让自动化测试重新跑通,开发者不得不打开网页的审查窗口,在代码树里一层层翻找新的标签名字。

经常出现改代码只花几分钟、修测试脚本反而要花大半天的情况,很多开发者折腾几次之后就放弃了写端到端测试。

5. 用自然语言写端到端测试

随着智能体技术的发展,这种繁琐的交互有了更顺手的解法。

开源社区最近出现了一个叫 e2e 的新框架,把多模态与大语言模型直接引入了端到端测试流程中。

它不再需要开发者在底层代码里寻找选择器,而是允许你用日常的自然语言交代测试意图:

让 Agent 打开设置页面,把当前工作区升级为专业版;

然后再写一行确凿的结果检查,看看屏幕上的状态标签有没有变成 Pro。

大语言模型会根据当前的网页画面理解内容,找到对应的按钮并完成操作。

它在运行成本上也做了一个很务实的设计:录制与重放机制。

第一次用自然语言跑通流程后,框架会自动把操作步骤录制下来,固化成确定的轨迹。

在之后的日常检查中,系统直接快速重放录好的轨迹,不需要调用大模型,也不产生额外的 API 费用。

只有当页面发生了改动、老轨迹走不通时,它才会重新唤醒大模型进行自愈修复。

底层支持浏览器和手机模拟器,通过一行命令就可以初始化:

npx e2e init

6. 发布前的多层环境验证

在本地把单元测试和端到端测试都跑通之后,软件通常也不会直接发布给所有人。

因为开发者自己的电脑环境,和真实世界众多用户的设备与网络存在客观差异。

所以在软件正式面向全量用户之前,通常还要经过好几层专门的环境验证。

平时大家在各大软件官网上见到的各种版本名词,背后对应的正是这些测试阶段:

最早期的版本通常叫开发者预览版或每晚构建版(Nightly Build),专门给核心研发人员内部排查明显的故障;

稍微成型一点的会部署在阿尔法环境,也就是 Alpha 版本,主要面向团队内部员工和产品经理验证核心主流程;

功能基本做全之后,软件会部署到贝塔环境,也就是大家熟悉的 Beta 版本,面向外部首批真实种子用户开放公测,用来在各种不同的手机型号和网络环境下发现未知的边界问题;

在最终推给所有人之前,团队通常还会保留一层预发布环境(Staging),服务器配置和数据规模与正式环境保持一致,用来做上线前的全链路最终验证;

最后一关才是所有人日常使用的生产环境(Production)。

新版本上线时,通常还会配合金丝雀发布机制:先分流百分之一的真实流量试跑,确认各项指标平稳之后再逐步全量放开。

而在规范的团队流程里,这些测试并不是靠人每天手动去点运行的,而是挂在持续集成流水线上。

每次提交代码,云端服务器自动把整套测试跑一遍,全部通过才允许发布上线。

软件工程的底层逻辑就是:用写代码来实现想法,用写测试来维持确定性。

项目地址:

github.com GitHub - tester-army/e2e: Next generation e2e testing framework for web and mobile apps.