4
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-06

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

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コマンドを打つだけでテストを行うことができます。

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

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

AbortController について

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

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

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して使います。

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になります。

import.meta.mainを使ってserve.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();
}

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

実行方法
> deno run --allow-net=0.0.0.0:8000 ./serve.ts
listening on http://localhost:8000/
テスト
> 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)

まとめ

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

おまけ

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

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");
    });
  },
});
4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0