测试全绿,页面为什么仍会启动失败?别只检查生成文件里“有那段文字”

 2个月前     378  

文章目录

依赖升级后,构建成功,单元测试全绿,生成文件也存在,但页面打开后还是在启动阶段报错。很多人遇到这种情况,会先怀疑缓存、浏览器或部署环境。

最近一次真实排查中,问题更直接:提供方生成的脚本和消费方脚本各自都合法,但双方约定的全局变量名不一致。旧测试只在文件文本里寻找几个关键词,因此把“两个文件分别包含看起来正确的文字”误当成了“两个产物真的能够一起运行”。

两个文件都正确,不代表连接处正确

把现场抽象成一个最小例子。提供方生成:

globalThis.__GENERATED_CONTRACT__ = {
  version: "3.0.0",
  assets: ["shell.css", "shell.js"]
};

消费方却读取:

const lock = globalThis.GeneratedContract;
if (!lock) throw new Error("runtime contract missing");

这两段 JavaScript 单独看都能通过语法检查。第一段确实写入了一个对象,第二段也确实检查了一个对象。若测试只断言两个文件里出现了 version、assets 或 runtime contract 等文字,它们甚至可以稳定“全绿”。

但页面真正运行时,连接处没有任何模糊空间:一个写入 __GENERATED_CONTRACT__,另一个读取 GeneratedContract,就是接不上。

旧测试为什么没有抓住它

旧测试验证的是文本迹象,不是运行时行为。常见写法包括:

assert.match(providerSource, /version/);
assert.match(consumerSource, /runtime contract/);

这类测试适合检查提示语、注释清理或某个声明是否存在,却不适合证明跨文件合同。它至少漏掉了四件事:

  1. 双方使用的属性名是否完全一致;
  2. 两个脚本是否按真实顺序执行;
  3. 读取到的值是否具有预期类型和字段;
  4. 缺失、重命名或形状改变时是否会正确失败。

问题的本质不是“测试太少”,而是测试层级低于故障层级。故障发生在两个产物的运行连接处,测试却停留在各自产物的字符串表面。

最小修复:让产物在同一个隔离上下文里真的跑一次

Node.js 的 node:vm 模块可以在 V8 虚拟机上下文中编译并运行代码。[1] 对受信任的本地构建产物,可以利用它做一个很小的可执行合同探针:

import vm from "node:vm";
import assert from "node:assert/strict";

const context = vm.createContext({});

new vm.Script(providerBundle).runInContext(context);
new vm.Script(consumerProbe).runInContext(context);

assert.equal(
  context.__RESULT__.version,
  "3.0.0"
);
assert.deepEqual(
  context.__RESULT__.assets,
  ["shell.css", "shell.js"]
);

这里要验证的不是示例中的具体名字,而是三层行为:

  • 提供方在真实执行后暴露了什么;
  • 消费方按真实加载顺序读到了什么;
  • 读到的对象是否满足名称、类型和关键字段合同。

node:test 是 Node.js 的稳定测试模块,用于创建 JavaScript 测试;测试文件本身也必须能由 Node.js 执行。[2] 因此这类探针可以直接放入现有测试套件,而不需要为一个小合同先搭建完整浏览器环境。

但不要把 vm 当成安全沙箱

Node.js 官方文档明确提醒,node:vm 不是安全机制,不应拿来运行不受信任的代码。[1]

这里的适用边界是:运行你自己刚构建、来源受控的前端产物,目的是验证跨文件合同。若输入可能来自用户上传、第三方插件或未知网络内容,必须使用真正的进程、容器或浏览器隔离边界,不能因为名字里有“虚拟机”就误判为安全沙箱。

我最后保留了三层门,而不是只加一个测试

修复没有止于改对变量名。我保留了三层互补检查:

第一层:产物身份

校验依赖版本、关键文件清单和构建输出是否来自被锁定的那一份依赖。它回答“拿到的是不是预期产物”。

第二层:可执行合同

把提供方与消费方放进同一上下文,按真实加载顺序执行,断言共享对象的名称、类型和必要字段。它回答“连接处能不能工作”。

第三层:真实页面冒烟

在无缓存浏览器里打开代表页面,确认首屏可见、控制台无启动错误、核心静态资源请求成功。它回答“真实用户路径是否成立”。

三层不能互相冒充。浏览器冒烟最接近用户,但失败后定位成本较高;可执行合同探针很快,却无法替代 DOM、CSS、网络和缓存检查;产物身份检查能防止拿错版本,却不能证明 API 没有漂移。

应该为哪些跨文件边界增加这种探针

最值得加的不是所有文件,而是那些满足以下条件的边界:

  • 一个构建步骤生成配置、清单或全局对象,另一个模块负责消费;
  • 生产环境依赖脚本加载顺序;
  • 字段名或对象形状会随着依赖升级变化;
  • 过去的测试主要依靠正则、快照或文件存在性;
  • 一旦断裂,页面会在初始化阶段整体失败。

一个好的负向用例也很重要:临时把提供方属性名改错,测试必须立刻失败。如果改错后测试仍然全绿,说明它还没有真正穿过合同边界。

结论

“文件里有这段文字”只能证明文本存在;“两个文件都能构建”只能证明它们各自成立。跨产物集成真正需要证明的是:在接近真实的执行顺序和上下文里,提供方暴露的东西正好是消费方读取的东西。

因此,遇到依赖升级后的假绿灯,不要机械增加更多字符串断言。先找到断裂发生在哪个运行边界,再放入一个最小可执行探针,补上负向反例,最后用真实浏览器走一次用户路径。这样增加的不是测试数量,而是测试的探测能力。

本文是 AI 辅助整理的原创工程实践内容,示例、变量名和版本均已匿名化或改写。

来源与适用边界

[1] https://nodejs.org/api/vm.html — Node.js VM documentation
[2] https://nodejs.org/api/test.html — Node.js Test runner documentation

暂无评论

暂无评论...