Chapter 11

テストを書こう

uki00a
uki00a
2022.01.29に更新

DenoはTypeScriptを標準でサポートしており、型チェックによってある程度の安全性は得られます。

しかし、TypeScriptの型チェックによってあらゆるバグを事前に取り除けるわけではありません。

例えば、プログラムのロジックそのものにバグが混入することは、型チェックだけでは防ぐことができません。もし自動化されたテストコードがなければ、本番環境にそのままバグを持ち込んでしまう可能性があります。

一般的に、コードは書いている時間よりもメンテナンスしている時間の方が長いと思います。自動化されたテストコードを用意することで、メンテナンス性が向上し、バグの発生頻度が減ることで、結果として開発速度の維持につながります。

この章では、Denoでテストを書く際の方法論について解説します。

テストの書き方

Denoはテストランナ(deno testコマンド)を提供しています。

ここでは、このdeno testコマンドを使用したテストの書き方について説明します。

ファイル名

deno testコマンドは、テストファイルの名前に一定の規約を設けています。

deno testコマンドは、以下のいずれかの規則に従って命名されたファイルをテストファイルとみなします。

  • ファイル名が次のいずれかに一致する
    • test.ts
    • test.tsx
    • test.js
    • test.mjs
    • test.jsx
  • ファイル名の末尾が次のいずれかに一致する
    • _test.ts
    • _test.tsx
    • _test.js
    • _test.mjs
    • _test.jsx
    • .test.ts
    • .test.tsx
    • .test.js
    • .test.mjs
    • .test.jsx

テストコード

以下のように、sum関数を含むsub.tsというファイルがあったとします。

export function sum(...numbers: number[]): number {
  return numbers.reduce((sum, x) => sum + x, 0);
}

sum_test.tsという名前でこの関数のテストを用意してみましょう。

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { sum } from "./sum.ts";

Deno.test("sum() adds given numbers", () => {
  const actual = sum(1, 2);
  const expected = 3;
  assertEquals(actual, expected);
});

Deno.test("sum() returns 0 when no arguments given", () => {
  const actual = sum();
  const expected = 0;
  assertEquals(actual, expected);
});

このように、テストケースはDeno.test関数を使って定義します。

記述したテストコードはdeno testコマンドで実行できます。

$ deno test
running 2 tests
test sum() adds given numbers ... ok (2ms)
test sum() returns 0 when no arguments given ... ok (1ms)

test result: ok. 2 passed; 0 failed; 0 ignored;
 0 measured; 0 filtered out (4ms)

このように表示された場合は、テストの実行に成功しています。

もし、テストに失敗すると、以下のようなエラーが出力されます。

$ deno test
running 2 tests
test sum() adds given numbers ... FAILED (3ms)
test sum() returns 0 when no arguments given ... ok (1ms)

failures:

sum() adds given numbers
AssertionError: Values are not equal:


    [Diff] Actual / Expected


-   3
+   4

    at assertEquals (https://deno.land/std@0.95
      ... 省略 ...

failures:

        sum() adds given numbers

test result: FAILED. 1 passed; 1 failed; 0 igno
red; 0 measured; 0 filtered out (5ms)

このようなエラーに遭遇した場合は、テストが成功するようにコードを修正する必要があります。

アサーション

アサーション関数は、deno_stdのtestingモジュールで提供されています。

deno_stdを使ってみようのページで各関数を紹介していますので、詳しくはそちらを参照ください。

パーミッションを指定する

deno testコマンドには、deno runコマンドと同様に、パーミッションを指定することができます。

$ deno test --allow-read --allow-write

特定のテストファイルのみを実行する

deno testの引数としてファイル名を指定すると、そのファイルで定義されたテストケースのみが実行されます。

$ deno test ./tests/general_test.ts

また、複数のテストファイルを指定することもできます。

$ deno test ./tests/general_test.ts ./tests/connection_test.ts

特定のテストケースのみを実行する

特定のテストケースのみを実行したいときは、--filterオプションを使いましょう。

例えば、以下のコマンドを実行すると、名前[1]parseが含まれるテストケースのみが実行されます。

$ deno test --filter parse

また、--filterオプションには正規表現を指定することもできます。

$ deno test --filter "/parse\s+.+\s+URL/"

この場合、/parse\s+.+\s+URL/の正規表現にマッチするテストケースのみが実行されます。

非同期処理のテスト

Denoのテストランナは、非同期処理のテストをサポートしています。

あるテストケースがPromiseを返却した場合、それがresolveされれば成功、rejectされれば失敗とみなします。

例えば、以下のテストケースは、テスト関数がrejectされたPromiseを返却するため、失敗とみなされます。

import { assertStrictEquals } from "https://deno.land/std/testing/asserts.ts";

Deno.test("This test case will fail", async () => {
  await Deno.writeTextFile("./sample.txt", "Hello Deno!");
  const actual = await Deno.readTextFile("./sample.txt");
  const expected = "Hello World!";
  assertStrictEquals(actual, expected);
});

このコードのconst expected = "Hello World!";const expected = "Hello Deno!";に直すと、テストが成功するようになります。

リソースリークについて

Denoのテストランナはリソースリークのチェック機構を備えています。

テストケースの開始時点と終了時点でのアクティブなリソース数などを比較し、それらが一致しなければ、そのテストケースを失敗とみなします。

例えば、次のようなコードがあったとします。

// この関数内で`file`は開放されない想定
async function processFile(path: string): Promise<void> {
  const file = await Deno.open(path);
  // ... 省略 ...
}

Deno.test("processFile", async () => {
  await processFile("./sample.txt");
});

processFile関数でDeno.openを実行し、ファイルを開いています。このファイルは、このprocessFile関数内で適切に開放されていなかったとします。

このようにリソースを適切に開放していないコードが存在すると、テストの実行時にTest case is leaking resources.というエラーが発生します。

running 1 tests
test processFile ... FAILED (2ms)

failures:

processFile
AssertionError: Test case is leaking resources.
Before: {
  "0": "stdin",
  "1": "stdout",
  "2": "stderr"
}
After: {
  "0": "stdin",
  "1": "stdout",
  "2": "stderr",
  "3": "fsFile"
}

Make sure to close all open resource handles re
turned from Deno APIs before
finishing test case.
    at assert (deno:runtime/js/06_util.js:33:13
)
    ... 省略 ...

failures:

        processFile

test result: FAILED. 0 passed; 1 failed; 0 igno
red; 0 measured; 0 filtered out (3ms)

このエラーを解消するためには、適切にリソースを開放してあげる必要があります。

processFile関数を以下のように修正してみましょう。

async function processFile(path: string): Promise<void> {
  const file = await Deno.open(path);
  try {
    // ... 省略 ...
  } finally {
    file.close(); // ちゃんとファイルを閉じる!
  }
}

関数の終了時にfile.close()を呼び、適切にリソースが開放されるように修正しました。

この状態でテストを再実行すると、テストが成功するはずです。

テストを実行してみて、もしこのようなエラーに遭遇した際は、「リソースの開放漏れはないか?」「解決されていないPromiseはないか?」などをしっかりチェックしてみましょう。

テストダブル

テストを書いていると、包括的な機能を提供するテストダブルライブラリが必要になるケースがあるかもしれません。

Node.jsでよく使われるSinon.jstestdouble.jsなどのテストダブルライブラリは、Denoでも動作します。

そのため、テストダブルが必要になった際は、これらのパッケージの使用を検討してみるとよいでしょう。

以下にtestdouble.jsとSinon.jsの使用例を掲載します。

testdouble.js

// @deno-types="https://esm.sh/testdouble@3.16.1/index.d.ts"
import * as td from "https://esm.sh/testdouble@3.16.1/dist/testdouble.js?no-check";

class Article {
  constructor(readonly id: number, readonly title: string) {}
}

interface ArticleRepository {
  add(article: Article): Promise<void>;
  get(articleID: number): Promise<Article>;
}

const repository = td.object<ArticleRepository>();
const article = new Article(1, "foobar");
repository.add(article);
td.verify(repository.add(article));

Sinon.js

deno_stdのリポジトリにSinon.jsの使用例があります。

Sinon.jsを利用する際は、こちらの例を参考にするとよいと思います。

ポイント

  • Denoは組み込みのテストランナを提供しています。
  • リソースリークのチェック機能を使って、リソースリークを防止しましょう。
脚注
  1. Deno.test(name, fn)name引数で指定した値のことです。 ↩︎