我是写业务代码的。做这套东西之前,我没写过测试用例,一条都没有。
有一段时间权限相关的 bug 出得特别密。今天某个角色能调不该调的接口,明天跨租户能看到别人家的数据。每次都是线上有人反馈才知道,修完也不敢说修干净了,因为换个角色、换个接口,同样的坑还会再来一遍。
真正让我下决心的是,我发现我没有任何手段回答这样一个问题:改了这行代码,原来能用的地方还活着吗?
这篇文章是那之后两个月的事。103 条手写用例,最后跑成 31 条自动用例,一次 4 秒,挖出 6 个缺陷,现在每周一自动跑一遍。
一、先写的是一份文档
一开始我也去看了 pytest,看了几篇教程,写了个 test_login() 跑通了,然后就卡住了——不知道下一步该写什么。
后来我想明白了:卡住不是因为不会写代码,是因为我没想清楚要验证什么。
所以我调了个方向,先花一天半手写了一份用例文档,103 条,每条 9 个字段:
ID / 场景 / 角色 / 前置条件 / 步骤 / 预期结果 / 实际结果 / 状态 / 备注
写完这份文档,后面写自动化几乎就是「翻译」了。现在回头看,这一步省掉的是后面反复改结构的时间——直接上代码的话,很容易一路写成「把接口调一遍看看通不通」,那是冒烟,不是测试。
文档按 14 个模块分:
| 模块 | 条数 | 模块 | 条数 |
|---|---|---|---|
| 租户隔离 | 11 | 牛只 | 10 |
| 诊断 | 10 | 权限 | 9 |
| 投喂 | 9 | 库存 | 8 |
| 认证 | 6 | 菜单 | 6 |
| 成本 | 6 | 设备 | 6 |
| 审批 | 6 | 数据隔离 | 6 |
| 集团 | 4 | 端到端 | 6 |
写的时候我特意加了三样东西:术语的大白话对照表、账号和数据准备清单、缺陷断言的速查表。
当时是因为自己对测试这一套完全没概念,不得不边查边记。结果这份「小白笔记」后来帮了别人——产品拿去对着看,提了不少我漏掉的场景。
二、从一份文档到 4 个文件
落地是四个文件:
| 文件 | 职责 |
|---|---|
config.js | 环境配置:base URL、数据库、租户编码、各角色账号 |
lib.js | 8 个基础能力 |
tcs/index.js | 用例本身 |
run.js | 执行器:跑用例、出报告 |
lib.js 里就这点东西,每加一个都是被坑出来的:
login · loginAndSelectTenant · rawFetch · dbQuery · assert · runCase · invalidateToken · CFG
用例本身写起来很短:
TCS.push({
id: 'TC-PERM-003',
module: 'PERM',
title: '场长直调新增牛只接口被拒(无 cattle:add)',
expected: '返回 403,不产生数据',
async run() {
const t = await login('director');
const r = await rawFetch('/cattle', { method: 'POST', token: t, body: {...} });
assert(r.status === 403, '越权未被拦截: ' + r.text.slice(0, 120));
return 'code=' + r.status;
}
});
第一批 24 条,跑完 4 秒。
4 秒这个数字当时挺意外的。后来想通了:接口测试就是发 HTTP 请求、断言响应,没有浏览器、没有渲染、没有等待。跑得这么便宜,才有可能天天跑——这是它后来能挂到定时任务上的前提。
三、几条规矩,有的是踩了坑才明白的
只做只读的
tcs/index.js 第一行我写了个注释:
范围:只读安全子集 + 403 权限验证(不做写操作,避免污染数据)
这条不是设计出来的,是吃了亏才加的。
最早我想一次做全,增删改都测。跑了几轮之后,环境里的垃圾数据越堆越多,列表接口的断言开始莫名其妙地挂,账号也被锁过。那套东西就没人敢跑了。
改成只读之后,跑一百次环境还是干净的。写操作用例当然也能写,但得配套自造数据、自己断言、自己清场,那是后面的事,不是第二天就该干的事。
缺账号就跳过,别报失败
不是每个环境都有全套测试账号。缺的时候我让用例直接抛 SKIP:
if (!CFG.accounts.multiTenant) throw new Error('SKIP: 当前环境无多租户账号');
这个事的道理很简单:第一次跑出来 10 个红的,全是环境问题;第二次你就不认真看报告了;等到真有 bug 那天,你也不会看。
编号对着需求文档写
TC-AUTH-001、TC-PERM-003,编号跟需求文档对得上。
刚开始是强迫症,后来发现有用:某条用例挂了,我能顺着编号翻回去看当初的需求是怎么写的、我为什么这么断言。不然过两个月,自己写的用例自己看不懂。
每条用例都重新登录
这条纯粹是被逼的,原因在下面坑二。
四、折腾最久的是环境
写用例的时间大概三天,调环境的时间比这还长。下面这五个,前四个都跟业务逻辑没关系。
有两个账号,永远登不进去
测试账号里有两个,用户名 21 个字符,而系统的 USERNAME_MAX_LENGTH = 20。
它们永远登录失败,报「用户名不在指定范围」。
这地方我绕了很久。因为它看起来像权限问题,像密码问题,像租户没绑,唯独不像「名字太长」——谁会想到登录不了是因为用户名超长?而且报错信息也没直接说。
后来是拿一个能登的账号和它逐字对比,才发现长度差了 1。
测试数据也是数据,系统的约束一样管着你。
选完租户的 token,不能再选租户
系统是多租户的,登录后要选租户,选完换一个新 token。而那个新 token 是完整的租户态,再调 select-tenant 会出问题。
第一版框架没管这个,结果连跑两条需要选租户的用例就挂。表现很迷惑:单跑都过,连着跑就失败。
解法就是笨办法,每条用例开始前重新登录一次,不复用 token。代价是 4 秒变 5 秒。我认了,因为排查「为什么连着跑会挂」花的时间,比这 1 秒贵太多。
登出不清缓存,后面全 401
logout 之后如果不清服务端的 token 缓存,后面所有用例全部 401。
而且会连锁:框架一旦复用 session,第一条挂了,后面全跟着挂,看起来像「所有接口都坏了」。
第一次遇到时我以为权限配置被谁动了,查了半天配置。
后来加了 invalidateToken(),登出后显式清缓存。
反复用错密码,把真账号锁了
这个坑我觉得最阴。
系统有登录失败锁定:连续输错密码锁 10 分钟。计数在 Redis 里(pwd_err_cnt:<username>),重设密码也不清这个计数。
我写「错误密码应该被拒」这条用例时,用的是真实的测试账号。跑了几十次之后,那个账号被锁了,后面一串用例全登不上。
改法很朴素:这条用例改用不存在的用户名。反正验证的是「密码错被拒」,用个假账号一样能验,还不会在真账号上留痕迹。
由此得出一条通用经验:验证失败路径的用例,输入尽量用不会留下痕迹的。
用例的预期,是照两周前的数据写的
这条最尴尬。
我写权限用例时,角色和菜单的对应关系是查一份数据库快照来的。后来发现那份快照是两周前的,绑定关系早改了。
于是好几条用例的预期是错的。比如我断言「场长看不到在线诊断」,实际上当时他能看到——因为那本身就是个 bug,而快照那会儿这个 bug 还没出现。
用例报红,我以为是系统坏了,查了半天才发现是我自己的预期过时了。
后来改成连库实时查,不再用静态快照。
这件事改了我一个想法:用例的预期值也是要维护的东西。系统变了、需求变了,预期不跟着改,测试就会从帮你变成骗你。
五、它挖出来什么
本地和测试服务器都跑过,确认了 6 个:
| 编号 | 缺陷 | 性质 |
|---|---|---|
| P0 | 某角色越权读诊断列表 | 越权,测试服务器上复现,能读到别人的数据 |
| BUG-1 | 设备模块接口契约不一致 | 前后端路径/方法对不上 |
| BUG-2 | 耳标批量查询契约不一致 | 同上 |
| BUG-3 | 场长问诊列表不可用 | 有菜单、接口没权限 |
| BUG-4 | 财务成本列表不可用 | 权限收太死,正常功能也挂了 |
| — | 耳标操作权限缺失 | 按钮有、接口没权限 |
BUG-3 和 BUG-4 挺有意思,是一对反的:
- BUG-3:菜单给了,接口权限没给,点进去报错
- BUG-4:接口权限收得太紧,正经角色也用不了
所以权限这块只测「越权被拒」是不够的,还得测「该能用的能不能用」。收太松和收太紧,都是线上事故。
六个缺陷修完之后,每个都补了一条回归用例:
TC-MOD-004 契约缺陷修复回归
TC-MOD-005 场长问诊列表可用(BUG-3 修复回归)
TC-MOD-006 财务成本列表可用 + 越权解除(BUG-4 修复回归)
这是我从这件事里学到的最有用的一条:bug 修完不算完,变成用例才算完。不然同样的问题过半年还会再来一次,而你已经忘了当初怎么修的。
最后全套 31 条,双环境全绿,29 PASS + 2 SKIP。
六、让它自己跑起来
写完没人跑,等于没写。这话我在别的项目上吃过亏。
所以最后一件事是挂定时任务:
每周一 12:00 自动执行
AT_ENV=remote node run.js,只读巡检 31 条用例,生成报告,对着 6 个已修复缺陷看有没有回归。有 FAIL 才提示人工介入,不自动改代码、不自动改库。
两个地方我犹豫过:
一是只读。自动跑的东西如果会写数据,半夜跑挂了没人知道,第二天环境全是脏的。所以这条线不敢越。
二是要不要自动修复。听着很美,但一个能改东西的测试,本身就成了新的风险源。我最后只让它报告。
七、如果也是零基础
三个事,按重要性排:
先写文档,别急着上框架。 我一开始也去装 pytest 了,写了一个用例就不知道该写什么。文档逼你想清楚要验什么,代码只是执行。
第一版只做只读和越权验证。 贪多会把环境搞脏,搞脏之后就没人跑了,这套东西就死了。
修完的 bug 补一条回归用例。 这一条决定了你做的是一次性劳动还是资产。
至于语言,我用 Node 没什么讲究。项目前端就是 JS,_at 目录里本来就都是 node 脚本,顺手。后来还写了个小工具,扫前端 api/*.js 的 url 和 method,跟后端所有 Controller 的注解比对,找契约不一致——用 node 读前端源码确实方便些。
换成 pytest 或 Java 也一样,那三条跟语言没关系。
(本文用「某养殖 SaaS 平台」代指实际项目,接口路径、账号、IP 均已替换,不含任何真实客户信息。)