deno標準テストモジュールの使い方


概要

denoの標準ライブラリであるdeno_stdにはtestingモジュールが含まれています。

今回はその使い方について紹介します。

deno及びdeno_stdについては、下記バージョンを想定しています。

テストの書き方

sum.ts
export function sum(...numbers: number[]): number {
  return numbers.reduce((acc, x) => acc + x, 0);
}
sum_test.ts
import { test, runIfMain } from 'https://deno.land/[email protected]/testing/mod.ts';
import { assertStrictEq } from 'https://deno.land/[email protected]/testing/asserts.ts';
import { sum } from './sum.ts';

test(function returnsSumOfNumbers() {
  const actual = sum(1, 2);
  const expected = 3;
  assertStrictEq(actual, expected);
});

test(function returnsZeroWhenCalledWithNoArgs() {
  const actual = sum();
  const expected = 0;
  assertStrictEq(actual, expected);
});

runIfMain(import.meta);
  • テストファイルの名前にはサフィックスとして_testをつけます。(※後述)

テストケースの記述

testing/mod.tsでエクスポートされているtest関数を使用します。
下記いずれかの形式でテストを記述します。

関数形式

関数のnameプロパティがテストの名前として扱われます。

test(function returnsSumOfNumbers() {
  const actual = sum(1, 2);
  const expected = 3;
  assertStrictEq(actual, expected);
});

オブジェクト形式

nameプロパティでテストの名前、fnプロパティでテスト関数を指定します。

test({
  name: 'sum() returns sum of numbers',
  fn() {
    const actual = sum(1, 2);
    const expected = 3;
    assertStrictEq(actual, expected);
  }
});

非同期処理のテスト

テスト関数でPromiseを返却します。

返却したPromiseがresolveされた場合は成功、rejectされると失敗とみなされます。

アサーションに失敗するとAssertionErrorが投げられるため、テスト関数をasync関数として定義しておくと、期待どおりに動作してくれます。

test(async function shouldWorkProperly() {
  const actual = await someAsyncFunc();
  const expected = { msg: 'hello' };
  assertEquals(actual, expected);
});

コマンドラインからテストを実行する。

下記コマンドにより、ファイル名が_test.tsで終わるテストがまとめて実行されます。
(--allow-netオプションをつけないと、実行に失敗してしまうようです)

$ deno test --allow-net

require is not definedと表示され、テストが実行されない。

eslint等を使用している関係でnode_modulesディレクトリが存在すると、テストが失敗してしまうことがあります。

その際は、-eオプションで除外対象のディレクトリまたはURLを指定できます。(複数指定したいときは、コンマで区切る)

$ deno test -e './node_modules' --allow-net

アサーション

アサーション関数はtesting/asserts.tsファイルでexportされています。
それぞれの関数はアサーションに失敗すると、AssertionErrorを投げます。

各アサーション関数は、末尾のmsg引数でアサーション失敗時の出力メッセージをカスタマイズできます。

assert(expr: boolean, msg = "")

exprfalseのとき失敗します。

assertEquals(actual: unknown, expected: unknown, msg?: string)

actualexpectedの深い比較をし、一致しなければ失敗します。
(actualexpectedがオブジェクトであれば各プロパティを再帰的に比較、配列であれば各要素を再帰的に比較します。)

assertNotEquals(actual: unknown, expected: unknown, msg?: string)

assertEqualsの否定版
actualexpectedが一致すると失敗します。

assertStrictEq(actual: unknown, expected: unknown, msg?: string)

actualexpectedを厳密に比較(actual === expected)し、一致しなければ失敗します。

assertStrContains(actual: string, expected: string, msg?: string)

expectedactualの部分文字列であれば成功します。

assertMatch(actual: string, expected: RegExp, msg?: string)

actualexpectedで指定された正規表現にマッチすれば成功します。

assertArrayContains(actual: unknown[], expected: unknown[], msg?: string)

actualexpectedで指定されたすべての要素を含んでいれば成功します。(要素の順番は問いません)

assertThrows(fn: () => void, ErrorClass?: Constructor, msgIncludes = "", msg?: string)

fnErrorClassで指定された型の例外を投げ、そのmessageプロパティにmsgIncludesで指定された文字列が含まれていれば成功します。

assertThrowsAsync(fn: () => Promise<void>, ErrorClass?: Constructor, msgIncludes = "", msg?: string)

fnErrorClassで指定された例外を投げるまたはrejectし、そのmessageプロパティにmsgIncludesで指定された文字列が含まれていれば成功します。

その他(Tips等)

特定のテストファイルのみを実行したい

sum_test.ts
import { test, runIfMain } from 'https://deno.land/[email protected]/testing/mod.ts';
import { assertStrictEq } from 'https://deno.land/[email protected]/testing/asserts.ts';
import { sum } from './sum.ts';

test(function returnsSumOfNumbers() {
  const actual = sum(1, 2);
  const expected = 3;
  assertStrictEq(actual, expected);
});

runIfMain(import.meta);

テストファイルの末尾でimport.metaを渡してrunIfMain関数を実行します。

すると、下記コマンドにより、対象のテストファイルのみを実行できます。

$ deno run ./sum_test.ts

テストファイルの配置場所について

このあたりはモジュールによって異なるようですが、現状下記のような方式をよく見かけます。

  • テスト対象モジュールと同一ディレクトリに配置する。
  • testsディレクトリに配置する。

タイムアウト

現状、タイムアウト機能はありませんが、下記のようなラッパーを用意すれば、対応できます。

function testWithTimeout(fn: () => void | Promise<unknown>, timeout: number = 3000): void {
  test({
    name: fn.name,
    async fn() {
      const timeoutPromise = new Promise((resolve, reject) => setTimeout(() => reject(new Error('Timeout exceeded')), timeout));
      const testPromise = fn();
      await Promise.race([testPromise, timeoutPromise]);
    }
  });
}

testWithTimeout(async function shouldWorkProperly() {
  const actual = await someAsyncFunc();
  const expected = { msg: 'hello' };
  assertEquals(actual, expected);
}, 2000);

サードパーティモジュールの管理について

現状、下記いずれかの手法を取ることが多いようです。

  • deps.tsというファイルを用意し、すべてのモジュールを一括管理する。
deps.ts
export { test, runIfMain } from 'https://deno.land/[email protected]/testing/mod.ts';
export { assertStrictEq } from 'https://deno.land/[email protected]/testing/asserts.ts';
import { test } from './deps.ts';
  • dinkdemのような専用ツールを利用する。