成为全栈·Taro 小程序篇·小程序质量验证:逻辑测试、真实后端与开发工具各证明什么
一份测试全绿,可以非常有用,也可以让人误判。差别在于我们是否知道它没有经过哪一层。

前言:编译没报错,按钮却不好点
小程序开发中出现过一个很实际的返工:界面文字容器看起来像操作入口,但在开发工具交互中不够稳定。改成原生 Button 后,预览切换、主题选择等动作才得到更明确的操作证据。
TypeScript 不会发现这个问题,构建也不会。另一方面,开发工具点几次又很难制造旧请求在账号切换后返回的精确时序。
所以,这篇不把所有验证都叫“测试通过”。我们按逻辑、真实后端、构建、工具和线上微信链路拆开,说明每份证据究竟保护什么。
先为结论选择对应证据
| 想证明的结论 | 合适证据 | 本轮情况 |
|---|---|---|
| 缓存旧请求不能回填 | 控制 Promise 顺序的逻辑测试 | 已有测试 |
| 他人不能改本人稿件 | 真实后端不同身份请求 | 已有本地集成 |
| 当前源码能生成 weapp | 类型与构建记录 | 已通过 |
| 预览按钮与目录可操作 | 开发工具实际点击 | 已观察具体样本 |
| 真实微信 code 可登录 | 工具 wx.login 加线上 callback | 修复后已成功 |
| 真机分享与相册完整可用 | 对应设备与平台操作 | 未逐项验收 |
第一条与第四条没有高低关系,它们只是回答不同问题。把一个逻辑测试叫端到端,会误导;因为没有真机就否认所有逻辑测试的价值,也同样不合理。
把每一层证据拆开看,它的射程与盲区其实很清楚:
| 证据层 | 代表证据 | 它能证明 | 它到不了的地方 |
|---|---|---|---|
| 逻辑层 | Node 单测 | 规则与完成顺序 | 平台控件与真机 |
| 集成层 | 临时 Hono 与 SQLite | 契约与业务路径 | 微信身份与相册 |
| 构建层 | typecheck、build:weapp | 源码可产出 weapp 包 | 运行期真实行为 |
| 工具层 | 开发工具实际点击 | 具体交互样本 | 完整设备矩阵 |
| 线上层 | 真实 wx.login | 平台交换链路 | 全能力与并发压力 |
这张表也是本篇的目录。后面每一节对应其中一行,先说明这份证据是怎么产生的,再补上它没有覆盖的部分。读者读完应该能自己判断:某个结论需要哪一层证据,而不必笼统地说“测试通过了”。
Node 测试保留真实逻辑,替换平台边界
测试脚本用 esbuild 打包 test/core.test.ts,将 Taro 别名替换成测试模块,并注入 API 常量:
await build({
entryPoints: ["test/core.test.ts"],
outfile: ".test-build/core.test.cjs",
bundle: true,
define: { __API_BASE__: JSON.stringify("https://api.example.com/api/v1") },
platform: "node",
format: "cjs",
alias: { "@tarojs/taro": resolve("test/taro-mock.ts") },
});
缓存、会话、解析和草稿使用实际模块,平台 request 与 storage 用可控制替身。这样测试关心规则,不需要为每个断言启动微信工具。
现有十一项覆盖读取合并、有界淘汰、失效栅栏、权限失败驱逐、单飞刷新、换账号、写入不重试、历史缓存、草稿、Markdown、评论与退出等重点。一个测试名称可能包含多个断言,不能拿“十一”当完备性比例。
当前没有独立覆盖 reaction store 和乐观回滚组件的专门测试,也没有所有 Hook 的渲染测试。文章不能把旁边相关测试算到不存在的覆盖上。
替身替换的正是那些会真正连到平台的地方。Taro 的 request 与 storage 在测试里换成可控制对象,于是缓存、会话、草稿这些模块仍运行真实代码,只有 IO 被接管。这样安排的好处是断言稳定:不必等待网络,也为每个用例省下一次启动微信工具的开销。
明确写出替身边界还有另一层意义:当某个缺陷只出现在平台侧时,逻辑测试很难提前暴露它。后面第七节的线上 500 正是这种情形,mock 本身没有问题,它只是本来就不覆盖那条平台参数差异。

一次"全绿"的测试,让我以为并发问题已经解决
写完并发相关代码之后,我跑了一遍测试,全绿。
而那两个真实 bug(登出后旧数据回填、并发刷新导致被登出),一个都没有被这些测试抓到。
我去查了原因,发现两个测试都长这样:
// ❌ 当时写的:三个请求"同时"发起,但实际是顺序 resolve
const a = api("/me/profile");
const b = api("/me/profile");
await Promise.all([a, b]); // 看起来测了并发,其实没有
api 的第一个 await 在内部,而 mock 的 resolve 是同步的 —— 于是 a 和 b 实际上一个跑完了另一个才开始。它测的是"两次顺序请求",不是"三个并发请求"。
改成真正控制顺序之后:
// ✅ 现在:显式控制 resolve 时机,让竞态真实发生
const wait = deferred<Envelope<Article>>();
const old = api("/me/profile"); // 发出但挂着
setSession(auth(2)); // 中途换人
wait.resolve(ok("private-old")); // 这时才返回
await assert.rejects(old, /账号已切换/);
区别只在于"resolve 发生在什么时候"。 而这一个差别,决定了这个测试是"测了并发"还是"假装测了并发"。
一个测试通过,不等于它验证了你以为它在验证的东西。 而判断的唯一方法是问:“如果我把这个 bug 引入回去,这个测试会红吗?”
这条判断标准现在是我看每个测试的第一问 —— 它比覆盖率数字有用得多。
竞态测试最重要的是完成顺序
缓存测试开始一个 deferred 请求,执行 clear,再释放旧响应,然后读取同键新值。关键断言是新读取必须得到新结果,而不是旧缓存。
const pending = c.read('a', () => wait.promise);
c.clear();
wait.resolve(1);
await pending;
assert.equal(await c.read('a', async () => 2), 2);
如果只测“读两次只调用一次 fetcher”,去重成立,失效仍可能错误。竞态必须把不利顺序明确制造出来。
账号测试同理:A 请求暂停,切到 B,释放 A 的成功响应,断言拒绝且当前会员仍是 B。它不是靠 sleep 等运气,而是控制异步边界。已有刷新测试用短延时形成重叠,断言仅刷新一次。
这些用例的共同之处是人为安排完成顺序,而不是等待一段时间。等待只能碰到“有时发生”的竞态,控制 Promise 才能稳定地把它变成必现路径,让失败可以复现、修复可以被验证。

真实本地集成跨过 mock,但没有跨过微信平台
integration.mjs 启动真实 Hono 应用,创建临时 SQLite 并执行迁移,监听 127.0.0.1:13997。客户端请求模块仍通过测试 Taro 适配向该服务发送请求。
脚本用临时账号验证注册、资料、固定 ID 投稿、提交撤回、点赞收藏历史、评论回复级联、双账号越权拒绝与密码登录退出。测试发布夹具只在临时进程存在,结束清理入口与数据库。
因此它能检查契约与业务,但不能证明原生相册、输入焦点或分享接收。更不能把本地密码登录当作真实微信平台身份验证。
集成依赖相邻 node-backend 安装好。启动失败、端口占用与测试执行错误都应保留日志,避免一份旧 integration.json 被拿来为新改动背书。
integration 的价值在于它真的跨过了 mock 这一层:请求经过契约、路由、数据库与迁移,返回的也是真实响应。代价是它依赖启动环境,所以脚本在 finally 里清理临时数据库与入口文件,防止一次失败污染下一次运行。
构建证据要与源代码版本配对
交付记录的执行顺序包括 typecheck、test、test:integration、build:weapp。产物扫描确认十四个注册页面、四个 Tab、线上 API 地址、临时夹具未进入包。
758869 字节是文件总和,不是平台上传包体,更不是冷启动性能。正式包体检查、分包和设备测量有自己的口径。
文章写作阶段没有改业务代码,所以引用已有交付记录,同时注明这是历史验证结果。本篇不会因重新写了一次命令,就声称刚刚重跑了所有测试。
如果代码继续变化,应该重新执行对应门禁、更新证据与提交关联。文档更新时间本身不能证明产物更新。
把证据和版本配对,比把证据和时间配对更可靠。同一份命令在不同提交上可能得出不同结论,因此记录里既有命令,也有它当时的实现快照,读者才知道这份绿是对哪一个版本成立的。
工具操作必须记录数据环境
线上只读验收观察首页与 M3-24 正文,包含表格、高亮和目录;写交互使用另一个隔离本机后端,端口 13998,操作登录、输入、预览、保存与提交,再核对同一个 ID。
两种环境不能混淆。若文章写“线上投稿通过”,读者会以为向生产写过测试文章,实际没有。工具结束后恢复线上基址,临时服务与文件清理。
当时最后收尾遇到锁屏,未能追加最终产物的全部界面复看。已有操作证据保留,最后微调靠类型和构建确认。之后真实微信专项又更新了登录证据。验收记录中的时间先后要读完整,不能只取旧段落里的“未验证”。
环境写清楚,结论才有边界。只读验收与写交互分别记录在案,读者能分辨哪些操作碰过真实数据、哪些只发生在一台本机临时服务上。这份区分对以后复核同样有用:需要复现时,照着环境描述就能搭出接近的现场。
真实微信登录补上了运行时一层
配置 Secret 后线上仍 500,Node mock fetch 无法暴露 Workers 的 redirect 参数差异。workerd 复现、修复 manual 并拒绝 3xx,再使用真实 wx.login 才形成完整专项证据。
这说明 mock 不是错误,而是边界有限。平台交换需要真实运行时样本,逻辑测试负责其可控制的安全规则,双方都应保留。
真实登录成功后,会员中心显示首次设置入口;没有代用户设置密码,真实重复登录与跨 APP 设密矩阵也没有全部执行。这些边界写进专项文档,才不会把一条成功链路扩成全能力声明。
专项文档把这个顺序记完整:先复现、再定位差异、再修复、最后用真实链路确认。任何一步省略,结论都会少一层支撑。这也解释了为什么单看早先的记录会得出“未验证”的印象——它只是还没走到最后那一步。
测试记录也可能让人误读,必须检查断言
一条名为“评论循环保护”的测试,具体断言是分组结果不会因循环丢失所有节点并能终止。它没有证明循环数据会被自动纠正成业务上唯一的树。阅读测试名称之后,还要阅读输入与断言。
同样,“网络失败不重试写请求”统计了一次调用,它保护的是当前请求封装不自动再次写入,不覆盖用户手动第二次点击或另一设备。名词范围越大,越需要解释断言实际到哪里。
新增回归应该从已暴露问题出发
若进一步修复非卡片区域点赞计数,应为对应消费者补渲染断言;若增加上传刷新重放,应模拟认证失败、旧身份与文件任务;若做完整离线阅读,应检查磁盘过期和清理后回填。
这比为每个 getter 增加同构测试更有价值。测试应该捕获一个可能回来的错误,而不是机械照抄实现,让两份同样错误的代码互相通过。
当前文章写作不修改业务代码,也没有为未修复边界补虚假的通过记录。它们作为明确后续场景留下,避免审阅者把事实说明误解成代码变更。
把证据与提交关联起来
报告记录运行环境、命令、目标地址与结果,文章记录开始写作的实现快照。后续代码变化时,可以判断哪些证据需要更新,而不是用一份不知年代的截图作结论。
生产写入尤其要隔离。测试发布夹具绑定本机临时进程,不应进入上线应用;线上只读与真实身份登录分别记录,不发布无意义测试文章,也不代用户设置真实密码来制造完成感。
一份严谨验收还要保留失败与中断。锁屏导致未补最后点击回归,就写中断;平台能力未测,就写范围。可信度来自结论可追溯,不来自报告中只有绿色对勾。
把未验收项写成下一次可以执行的工作
尚缺相册上传、原生分享实际接收、真机键盘、弱网长时间使用、恢复弹窗的独立操作证据。每项都应该有输入条件、动作与预期,而不是模糊写“后续完善”。
例如上传期间继续输入后提交,检查文字不被旧快照覆盖;恢复草稿拒绝覆盖,检查本机文本还在;账号切换释放旧响应,检查私人内容不串。这些能分别补齐行为与时间顺序。
更多发布范围见 演示与正式发布差距,整体判断见 第五批复盘。可信交付,不是声称没有未知,而是让每个结论都有对应证据、每个未知都有清楚位置。
延伸阅读
如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」:
🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html
📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/FungLeo/article/details/168963894




