服务端阅读 05月31日 17:20
Deno 的测试框架怎么用?异步、权限和覆盖率怎么处理?
Deno 的测试框架是内置能力,不需要先装 Jest、Mocha 这类第三方 Runner。核心用法是用 Deno.test() 注册测试,用标准库里的断言函数验证结果,再通过 deno test 运行。它的特点不是“功能最多”,而是和运行时权限、TypeScript、覆盖率、并发执行放在同一套命令里管理。真正写项目时,重点不只是会写一个 assertEquals,还要知道异步资源怎么清理、权限怎么最小化、哪些测试适合并行,哪些测试必须隔离。如果把 Deno 测试只理解成“能跑断言”,很容易写出本地偶尔通过、CI 经常失败的测试。Deno 的测试设计更强调运行时约束:测试代码默认没有额外权限,未关闭的异步操作会被检查,覆盖率也不需要额外插件。这些默认值会让刚上手的人觉得严格,但对长期维护很友好,因为很多隐蔽问题会在测试阶段就暴露出来。追问最小的 Deno 测试应该怎么写?最小测试通常放在 *_test.ts 或 .test.ts 文件里,Deno 会自动发现这些文件。断言建议优先从 jsr:@std/assert 引入,旧项目里也可能看到 https://deno.land/std/.../asserts.ts,两者不要在同一个项目里混着升级。测试名要描述行为,而不是写成 test add 或 works,否则失败时排查成本很高。边界上,assertEquals 会做深比较,适合对象和数组;如果要比较同一个引用,才用 assertStrictEquals。实际项目里还要注意导入路径的稳定性,特别是从远程 URL 迁移到 JSR 或 npm 兼容包时,锁文件和版本号要一起维护。否则同一段测试在不同机器上可能解析到不同版本的断言库,报错信息也会不一致。import { assertEquals } from "jsr:@std/assert";function add(a: number, b: number) { return a + b;}Deno.test("add returns sum of two numbers", () => { assertEquals(add(1, 2), 3); assertEquals(add(-1, 1), 0);});deno testdeno test src/math_test.tsdeno test --filter="add returns"异步测试、异常测试和资源清理有什么坑?异步测试的函数本身要 async,并且必须 await 被测 Promise,否则测试可能在异步错误抛出前就结束。同步异常用 assertThrows,Promise 拒绝用 assertRejects,这两个不要混用。Deno 默认会检查未关闭的 op、资源和异常退出,这比很多测试框架严格,也更容易暴露真实问题。踩坑最多的是启动 HTTP server、文件句柄或数据库连接后忘了在 finally 里关闭,短期看只是测试失败,长期看会让 CI 随机挂。如果确实有长连接或后台计时器无法在测试结束前自然关闭,可以临时关闭 sanitizeOps 或 sanitizeResources,但这应该是最后手段。更好的做法是把启动和关闭封装成工具函数,让每个测试都能明确释放资源。import { assertRejects } from "jsr:@std/assert";async function loadUser(id: number) { if (id <= 0) throw new Error("invalid id"); return { id };}Deno.test("loadUser rejects invalid id", async () => { await assertRejects(() => loadUser(0), Error, "invalid id");});测试里需要读文件、访问网络时权限怎么给?Deno 的测试和运行代码一样受权限模型约束,读文件要 --allow-read,访问网络要 --allow-net。可以在命令行给权限,也可以在单个 Deno.test 的配置里声明权限;后者更适合单元测试,因为它能把权限边界写在测试旁边。取舍是命令行授权简单,适合本地临时跑;单测级授权更啰嗦,但 CI 和代码审查时更安全。不要为了省事长期使用 --allow-all,它会掩盖代码偷偷访问文件、环境变量或网络的行为。权限还会影响 Mock 策略:能用内存假对象替代真实文件和网络时,就不要为了测试方便放开系统权限。这样做的代价是多写一点测试替身,但收益是测试更快、更稳定,也更接近单元测试的边界。Deno.test({ name: "reads fixture file", permissions: { read: ["./fixtures"] }, async fn() { const text = await Deno.readTextFile("./fixtures/user.json"); if (!text.includes("name")) throw new Error("bad fixture"); },});deno test --allow-read=./fixturesdeno test --allow-net=localhost:8000Deno 测试怎么组织才适合真实项目?单元测试可以贴近源码放,例如 src/user_test.ts;集成测试可以放到 tests/,并在 deno.json 里统一配置 include 和 exclude。测试写法上推荐 Arrange、Act、Assert 三段式,但不用机械地写注释,关键是让准备数据、执行动作、验证结果一眼能分开。涉及数据库、临时目录、端口监听时,每个测试都要创建自己的隔离环境,不能依赖前一个测试留下的状态。并行执行能节省时间,但共享全局变量、固定端口、固定文件名的测试不适合直接并行。当测试之间共享 fixture 时,fixture 可以只读共享,但运行中产生的数据最好写入临时目录。固定写 ./tmp/result.json 这类路径,在并行测试和重复运行时都容易互相污染。{ "tasks": { "test": "deno test --allow-read=./fixtures", "test:ci": "deno test --coverage=coverage --fail-fast" }, "test": { "include": ["src/**/*_test.ts", "tests/**/*.ts"], "exclude": ["vendor/"] }}覆盖率和 CI 里应该关注什么?覆盖率可以用 deno test --coverage=coverage 生成原始数据,再用 deno coverage 输出文本、LCOV 或 HTML。覆盖率适合发现“完全没测到”的分支,但不应该变成唯一目标,因为 90% 覆盖率也可能没有断言关键行为。CI 里更重要的是固定命令、固定权限、失败快速暴露,并把网络类测试和纯单元测试分开。一个常见取舍是本地默认跑快速单测,合并前或夜间任务再跑慢集成测试,这样不会让开发反馈变得太慢。覆盖率目录也要在 CI 中清理,避免上一次运行留下的数据影响这一次报告。对于分支很多的业务代码,可以把覆盖率报告当成提示,再回头检查断言是不是验证了业务结果,而不是只执行了代码。deno task test:cideno coverage coverage --lcov --output=coverage.lcovdeno coverage coverage --htmlDeno 测试框架的优势在于默认严格、命令少、和运行时边界一致。写好它的关键不是堆断言,而是把权限、资源清理、异步失败和测试隔离一起设计好。