公司接了一个电商促销系统的需求,时间紧任务重。按照以往经验,前端刚搭好页面,后端接口还没定稿,测试人员只能干等着,等联调时各种边界问题集中爆发,改一个bug冒两个新问题。这次我们决定换种方式:从第一天就开始写测试。
先写测试,再写实现
拿到需求文档后,开发团队没急着画架构图,而是和产品经理一起把核心流程拆成可验证的小功能点。比如“用户领取优惠券”这个动作,我们先写出这样的测试用例:
it('应允许未领取过的用户成功领取', () => {
const user = createUser('user123');
const coupon = createCoupon('CNY50');
expect(user.getCoupons()).not.toContain(coupon);
user.claimCoupon(coupon);
expect(user.getCoupons()).toContain(coupon);
});
it('不应允许同一用户重复领取同张优惠券', () => {
const user = createUser('user123');
const coupon = createCoupon('CNY50');
user.claimCoupon(coupon);
expect(() => user.claimCoupon(coupon)).toThrow('已领取');
});
这些测试一开始全是红的——因为方法根本没实现。但它们清晰定义了“什么才算正确”。接下来的任务就是让代码跑通这些用例。这种倒过来的工作流,逼着我们先思考接口设计,而不是埋头写逻辑。
重构不再提心吊胆
两个月后,运营提出要支持“阶梯式满减”,原来的优惠计算模块得大改。以前遇到这种情况,没人敢动旧代码,生怕牵一发动全身。这次不一样,每次调整完,运行一遍测试套件,几十个用例几秒内给出反馈。某个分支漏了金额为负的校验?测试立刻报错。改完再跑,绿了,心里就有底了。
有个同事原本觉得写测试是浪费时间,直到他发现自己的一个边界判断错误在提交前就被捕获了。他说:“这就像给代码买了保险,每次改动都踏实。”
沟通成本降了下来
测试用例成了团队的新语言。产品说“超时不能领取”,开发直接对应到新增一条测试:
it('活动结束后的优惠券不可领取', () => {
const expiredCoupon = createCoupon('CNY20', { endTime: yesterday });
const user = createUser('user456');
expect(() => user.claimCoupon(expiredCoupon)).toThrow('已过期');
});
QA看到这些用例,自然知道哪些情况必须覆盖,甚至能自己补充异常场景。测试文件成了活的需求文档,比Word文档直观多了。
项目上线前三天,我们跑通了全部1278个单元测试和集成测试。没有通宵救火,没有临时补丁。上线后监控显示核心流程成功率99.98%,那个曾被所有人视为“定时炸弹”的优惠发放功能,一次就过了。