为何处理拒绝Promise的Deno测试会出现不稳定(Flaky)问题?
问题场景
运行以下测试代码:
const file = "exp.txt"; Deno.test("handle rejected promise", async (t) => { try { await Deno.stat(file); } catch (e) { if (e instanceof Deno.errors.NotFound) { await Deno.writeTextFile(file, "some text"); } else { throw e; } } finally { await Deno.remove(file); } });
- 使用命令
deno test --allow-read --allow-write exp.test.ts运行时,测试成功。 - 添加
--trace-ops选项后,执行deno test --allow-read --allow-write --trace-ops exp.test.ts,测试失败并抛出错误:
./exp.test.ts (uncaught error)
error: (in promise) NotFound: The system cannot find the file specified. (os error 2), stat 'exp.txt'
This error was not caught from a test and caused the test runner to fail on the referenced module.
It most likely originated from a dangling promise, event/timeout handler or top-level code.
此外,更复杂的同类代码即使不添加--trace-ops也会失败,表现出竞态条件或超时相关的不稳定问题。当前使用Deno 1.28.3,多个1.2x版本均存在此不稳定现象,但1.28.3之前版本无该问题。注:这种通过stat+捕获NotFound错误检查文件存在性的方式,是Deno官方文档推荐的实现逻辑。
核心原因
问题本质是测试上下文销毁与异步文件操作的时序竞态:
- 当启用
--trace-ops时,测试 runner 的执行节奏被改变,finally块中的Deno.remove可能先于catch块中的Deno.writeTextFile完成,或者测试函数已执行完毕、上下文被销毁后,异步操作才抛出未被捕获的异常。 - 在复杂场景中,文件系统操作的天然延迟会触发同样的问题:测试 runner 认为测试已结束,但后续异步逻辑仍在运行,抛出的异常无法被测试的
try/catch捕获,最终触发全局未捕获错误。
解决方案
1. 明确控制异步操作生命周期
调整逻辑,确保测试函数退出前所有异步操作完成,且仅在文件确实存在时执行删除操作:
const file = "exp.txt"; Deno.test("handle rejected promise", async (t) => { let fileExists = false; try { await Deno.stat(file); fileExists = true; } catch (e) { if (e instanceof Deno.errors.NotFound) { await Deno.writeTextFile(file, "some text"); fileExists = true; } else { throw e; } } // 仅确认文件存在时执行删除,避免NotFound错误 if (fileExists) { await Deno.remove(file); } });
2. 使用官方标准库的exists方法
直接采用Deno官方推荐的exists方法(内部已优化竞态处理),替代手动stat+错误捕获的逻辑:
import { exists } from "https://deno.land/std/fs/mod.ts"; const file = "exp.txt"; Deno.test("handle file operations", async (t) => { try { if (!(await exists(file))) { await Deno.writeTextFile(file, "some text"); } } finally { // 删除前再次检查文件存在性 if (await exists(file)) { await Deno.remove(file); } } });
3. 用测试步骤锁定异步操作
使用t.step包裹异步逻辑,确保测试 runner 等待所有子步骤完成后再结束测试:
const file = "exp.txt"; Deno.test("handle rejected promise", async (t) => { await t.step("check and create file", async () => { try { await Deno.stat(file); } catch (e) { if (e instanceof Deno.errors.NotFound) { await Deno.writeTextFile(file, "some text"); } else { throw e; } } }); await t.step("clean up file", async () => { // 捕获删除时的NotFound错误,避免全局异常 try { await Deno.remove(file); } catch (e) { if (!(e instanceof Deno.errors.NotFound)) { throw e; } } }); });
4. 升级Deno版本
该问题在Deno 1.28.x版本出现,后续稳定版可能已修复测试 runner 的时序问题,建议升级至最新官方稳定版本。
内容的提问来源于stack exchange,提问作者Frank de Groot - Schouten

