【Deno】標準ライブラリを使ってテスタブルなサーバーを書く

以下のコードは、Deno標準ライブラリを使って書いた単純なHello Worldサーバーです。

```ts:serve.ts
import { serve } from "https://deno.land/std@0.117.0/http/server.ts";

console.log("listening on http://localhost:8000/");
serve(() => new Response("Hello World\n"));
```

Denoには組み込みのテストツールが備わっており、`test.ts`を作成して`deno test`コマンドを打つだけでテストを行うことができます。

```ts:test.ts
import "./serve.ts";
Deno.test("server test", ()=>{
  // ???
})
```

さて、先ほどのHello Worldサーバーをテストするには、どのようなコードを書けばいいのでしょうか？

## AbortController について

まず、テストが終了した後にサーバーを止める手段が必要です。
サーバーを停止させるには、Web標準のAPIである`AbortController`を使用します。
`AbortController`を`serve()`関数の第2引数に渡してあげることで、外部からサーバーを停止させることができます。

https://doc.deno.land/deno/stable/~/AbortController

更に、`test.ts`からサーバーを起動・停止を管理するために、全体を関数で囲って`export`します。

```ts:serve.ts
import { serve } from "https://deno.land/std@0.117.0/http/server.ts";

console.log("listening on http://localhost:8000/");

// export for test
export function startServer() {
  const controller = new AbortController(); // サーバーを停止させるためのAbortControllerを生成
  const server = serve(() => new Response("Hello World\n"), controller);
  return {
    server, // サーバーが終了した時に解決されるPromise
    controller, // controller.abort()を呼ぶとサーバーが停止する
  };
}
```

`test.ts`では、`startServer`関数をimportして使います。

```ts:test.ts
import { assertEquals } from "https://deno.land/std@0.117.0/testing/asserts.ts";
import { startServer } from "./serve.ts";

Deno.test({
  name: "server test",
  async fn() {
    // サーバー起動
    const { server, controller } = startServer();

    try {
      // fetch()を使ってlocalhostへアクセスし、正しいレスポンスが返ってくるか確認
      const response = await fetch("http://localhost:8000/");
      assertEquals(await response.text(), "Hello World\n");
    } finally {
      // サーバー停止
      controller.abort();
      await server; // サーバーの終了を待つ
    }
  },
});
```

ここで、最後の行で`await server`しているのに注意してください。
この`server`という変数の中には、「サーバーが終了したときに解決されるPromise」が入っています。このPromiseが解決されるのを待たないと、deno testコマンドで実行した時にエラーが出る可能性があります。

## import.meta.main について

上のコードではまだ不完全で、`serve.ts`を実行しても`startServer()`が呼ばれないため、サーバーが起動しません。そのため、

- `serve.ts`から実行された時は`startServer()`関数を実行**する**
- `test.ts`から実行された時は`startServer()`関数を実行**しない**

という風にしなくてはいけません。ここで`import.meta.main`の出番です。

`import.meta.main`はDeno固有のプロパティで、「自身のモジュールがメインモジュールかどうか」を返します。
メインモジュールならば`true`、他のファイルからimportされた時は`false`になります。

https://doc.deno.land/deno/stable/~/ImportMeta#main

`import.meta.main`を使って`serve.ts`を書きなおすと、以下のようになります。

```ts:serve.ts
import { serve } from "https://deno.land/std@0.117.0/http/server.ts";

console.log("listening on http://localhost:8000/");

// export for test
export function startServer() {
  const controller = new AbortController();
  const server = serve(() => new Response("Hello World\n"), controller);
  return {
    server, // サーバーが終了した時に解決されるPromise
    controller, // controller.abort()を呼ぶとサーバーが停止する
  };
}

if (import.meta.main) {
  // メインモジュールの時のみサーバーを起動
  startServer();
}
```

以上で、標準ライブラリのサーバーをテストすることができました。以下のコマンドで実行とテストを行うことができます。

```shell:実行方法
> deno run --allow-net=0.0.0.0:8000 ./serve.ts
listening on http://localhost:8000/
```

```shell:テスト
> deno test --allow-net=0.0.0.0:8000,localhost:8000 ./test_test.ts
listening on http://localhost:8000/
running 1 test from file:///C:/Users/azusa/work/deno/test/test_test.ts
test server test ... ok (360ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (395ms)
```

## まとめ

標準ライブラリのサーバーをテストするには、`AbortController`と`import.meta.main`を使います。

## おまけ

複数のURLに対するテストを行うには、サーバーの起動・停止部分を関数に括り出しておくと便利です。

```ts:test.ts
import { assertEquals } from "https://deno.land/std@0.117.0/testing/asserts.ts";
import { startServer } from "./serve.ts";

async function testServer(
  path: string, // テスト対象のパス
  fn: (res: Response) => void | Promise<void>, // テスト内容を指定する関数
) {
  // サーバー起動
  const { server, controller } = startServer();
  try {
    const response = await fetch(new URL(path, "http://localhost:8000/"));
    await fn(response);
  } finally {
    // サーバー停止
    controller.abort();
    await server;
  }
}

Deno.test({
  name: "server test path1",
  async fn() {
    await testServer("/path1", async (response) => {
      // `/path1`にアクセスした時のレスポンスが`response`変数に入っている
      assertEquals(await response.text(), "Hello World\n");
    });
  },
});

Deno.test({
  name: "server test path2",
  async fn() {
    await testServer("/path2", async (response) => {
      assertEquals(await response.text(), "Hello World\n");
    });
  },
});
```