321 项断言,零测试框架:纯逻辑必须零 import

我的自检是 node scripts/selftest.ts 直接跑的,没有测试框架也没有依赖。代价是纯逻辑模块一个相对 import 都不能有,而这个代价换来了代码形状。

这个站的自检是这么跑的:

npm run test
# = node scripts/selftest.ts

没有 Vitest、没有 Jest、没有测试框架,也没有任何测试依赖。321 项断言,直接跑。能这么干的原因和代价是同一件事:所有参与自检的模块,一个相对 import 都不能有。

为什么零 import 是硬要求

Node 从某个版本开始能直接跑 TypeScript —— 但它做的是类型擦除,不是模块解析。也就是说:

import { parse } from './parse';     // ✗ ERR_MODULE_NOT_FOUND
import { parse } from './parse.ts';  // ✓ 得写全后缀

我自己的测试文件可以老老实实写全 .ts 后缀。但我被测的那些模块如果互相 import,那就得在每个 import 上都写后缀 —— 而它们同时还要被 Astro / Vite 打包,在那边写 .ts 后缀是另一套麻烦。

所以规则被我简化成了一条最好记的形式:

games/*/logic/*tools/*/logic/*lib/i18n.tslib/detect.ts —— 这些模块零 import

换来了什么

① 一套不需要维护的测试设施。 没有框架就没有版本升级、没有配置、没有插件兼容。加一个游戏就是加一个 logic/ 目录,然后往 selftest.ts 里加几段断言。

② 断言就是代码。 没有 describe / it 套娃,一个小函数:

function eq(name: string, actual: unknown, expected: unknown) {
  if (Object.is(actual, expected)) { passed++; return; }
  failed++;
  console.log('FAIL: ' + name + '\n  期望 ' + expected + '\n  实际 ' + actual);
}

输出就是一行 passed: 321, failed: 0。失败时会打出期望值和实际值。

③ 反馈足够快。 整个自检不到一秒,所以它可以放在构建之前当门禁,而不是“CI 里等着看”。

代价是刻意的重复

零 import 意味着不能抽公共模块。真实例子:bytesToHex 这种三行函数,在哈希工具和编解码工具里各存了一份。抽成 lib/bytes.ts 就会让两个 logic/ 目录产生 import,自检立刻挂掉。

重复优于耦合 —— 在这里不是口号,是被工具链逼出来的选择。

三行函数重复两份,我接受。真正需要警惕的是重复的规则,比如「哪些字符串算语言代码」。这个不能靠复制粘贴,得靠编译期护栏:

// lib/lang.ts
// 编译期护栏:detect.ts 能判定的语种必须和 i18n.ts 的 Locale 完全一致。
// 两边漂移时这两行会直接编译报错(双向都查)。
export const DETECTABLE_IS_LOCALE: readonly Locale[] = DETECTABLE;
export const LOCALE_IS_DETECTABLE: DetectLocale[] = [...LOCALES];

detect.ts 零 import,所以它自己声明了一份能判定的语种清单;i18n.ts 也零 import,所以它自己声明了 Locale。两个清单靠这两行互相赋值,谁多一个少一个都编译不过。这是「重复」和「一致性」唯一能同时成立的方式。

真正的收益是代码形状

这条规则最值钱的地方不是测试,而是它逼出了分层

  • 测试跑在纯 Node 里,没有 DOM、没有 canvas、没有 localStorage
  • 所以任何碰这些的代码不能下沉到 logic/,只能留在 App.vue 里。
  • 于是每个游戏/工具天然就是「纯逻辑 + 一层薄薄的外壳」。

这个分层本来就该有,但“应该”通常敌不过“顺手写在一起”。把自检做成零依赖之后,它变成了有工具在拦的约束:你要是把 canvas 逻辑写进 logic/,下次跑自检就炸。

它测不到什么

诚实说清楚边界:

测得到 测不到
游戏规则、计分、碰撞判定 canvas 实际画出来的东西
工具的全部纯函数 剪贴板、拖拽、文件上传
语言判定、时区兜底 主题切换、水波纹、滚动高亮
waka 文本解析、时长换算 布局、响应式、任何视觉

碰 DOM 的那一半我不用自动化测,做法是把它做薄:逻辑全在 logic/ 里被测穿,外壳只剩“把事件接上、把结果画出来”。外壳出错的概率因此低到可以靠点一遍确认。

测不到的东西,最好的处理方式是让它没有可测的内容。

321 项断言的分布大致是:5 个游戏的规则、10 个工具的函数、语言与首访判定、GitHub 主页那段编码活动的解析。它们全都能在一次 node 调用里跑完,因为它们依赖的东西是:没有。

← 返回文章列表

评论