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.jsやtestdouble.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は組み込みのテストランナを提供しています。
- リソースリークのチェック機能を使って、リソースリークを防止しましょう。
-
Deno.test(name, fn)
のname
引数で指定した値のことです。 ↩︎