deno test で `Test case is leaking async ops.` が出た時の対処法

deno testコマンドには、test sanitizerという機能があります。この機能は

- Promiseの待ち忘れ
- ファイルやWebSocketの閉じ忘れ

などをチェックしてくれる機能です。
このチェックに引っかかると「Test case is leaking async ops.」や「Test case is leaking 1 resource:」「Test case attempted to exit with exit code: 0」と怒られます。

## それぞれのエラーの意味

- 「Test case is leaking async ops.」：Promiseを全てawaitしていない（テスト終了時にresolveされていないPromiseが残っている）
- 「Test case is leaking 1 resource」：ファイルやWebSocketなど、closeしなければいけないものをcloseしていない
- 「Test case attempted to exit with exit code: 0」：テスト中に`Deno.exit()`が呼び出されたため、後続の`Deno.test()`が実行されない


## 「Test case is leaking async ops.」の対処法

「Test case is leaking async ops.」はPromiseの待ち忘れがあるときに発生します。

```ts:test.ts
import { delay } from "https://deno.land/std@0.133.0/async/mod.ts";

Deno.test(function test() {
  /* await */ delay(100); // ここでawait していないためエラー
});
```

```shell
> deno test ./test.ts
running 1 test from file:///C:/Users/azusa/work/deno/test/test.ts
test test ... FAILED (20ms)

failures:

test
Test case is leaking async ops.

- 1 async operation to sleep for a duration was started in this test, but never completed. This is often caus
ed by not cancelling a `setTimeout` or `setInterval` call.

To get more details where ops were leaked, run again with --trace-ops flag.

failures:

        test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (60ms)

error: Test failed
```

例は単純なコードでしたが、大きいコードベースではどのPromiseに対してawaitを忘れているのかが一目では分からないことがあります。
そういう時は **`--trace-ops`フラグを付けて再実行する**ことで、どのPromiseを待ち忘れたかを知ることができます。

```shell
> deno test --trace-ops ./test.ts
running 1 test from file:///C:/Users/azusa/work/deno/test/test.ts
test test ... FAILED (30ms)

failures:

test
Test case is leaking async ops.

- 1 async operation to sleep for a duration was started in this test, but never completed. This is often caus
ed by not cancelling a `setTimeout` or `setInterval` call. The operation was started here:
    at Object.opAsync (deno:core/01_core.js:164:42)
    at runAfterTimeout (deno:ext/web/02_timers.js:215:31)
    at initializeTimer (deno:ext/web/02_timers.js:181:5)
    at setTimeout (deno:ext/web/02_timers.js:318:12)
    at https://deno.land/std@0.133.0/async/delay.ts:23:15
    at new Promise (<anonymous>)
    at delay (https://deno.land/std@0.133.0/async/delay.ts:14:10)
    at test (file:///C:/Users/azusa/work/deno/test/test.ts:4:3)
    at testStepSanitizer (deno:runtime/js/40_testing.js:444:13)
    at asyncOpSanitizer (deno:runtime/js/40_testing.js:145:15)

failures:

        test

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (215ms)

error: Test failed
```

見るべき場所はスタックトレースで、下から2行目にあるfile:///始まりのパスがあなたのコードに関する部分です。ここに表示されているファイル名と行数を辿ると、awaitしていないPromiseがあります。

:::note warn
`--trace-ops`オプションを付けると詳しいエラーが表示される分、実行速度が遅くなるようです。CIなどでは`--trace-ops`オプションを付けずに実行して置き、エラーが出た場合に限り`--trace-ops`オプションを付けるとよいでしょう。
:::

## 修正できない場合の回避策

このエラーが出る原因が別ライブラリにあり、修正が難しい場合、`Deno.test()`にオプションを渡してこのエラーを無視することができます。

```ts
Deno.test({
  name: "test",
  fn() {
    Deno.open("./a.ts");
  },
  // sanitizeOps: false, // 「leaking async ops」 を無視
  // sanitizeResources: false, // 「leaking resource」を無視
  // sanitizeExit: false, // 「exit with exit code: 0」を無視
});
```

`sanitizeOps`/`sanitizeResources`/`sanitizeExit`それぞれのオプションを`false`に設定すると、test sanitizerが無効になります。

ただし`sanitizeExit`を`false`にしてエラーを無視すると、後続のテストが実行されなくなるので注意が必要です。

```ts
Deno.test({
  name: "test",
  fn() {
    Deno.exit();
  },
  sanitizeExit: false,
});

Deno.test(function name() {
  // このテストは実行されない（上のDeno.exit()で強制終了されるため）
  console.log("aaa");
});
```

## まとめ

- deno testコマンドで発生する「Test case is leaking async ops.」や「Test case is leaking 1 resource:」「Test case attempted to exit with exit code: 0」エラーはtest sanitizerが検出したもの
- Promiseのawait忘れの該当箇所を発見するには`--trace-ops`フラグを使う
- これらのエラーを無視するにはオプションの`sanitizeOps`/`sanitizeResources`/`sanitizeExit`を`false`に設定する