deno test

denoのテスティング機能は、ツールを追加インストールせずに動作するテストランナーを提供しています。deno testサブコマンドで、ファイルやディレクトリ単位のテストスイートを一括実行できます。

denoはブラウザ環境との互換性が高く、ブラウザ向けのコードをテストできます。WebWorkersやWebAssemblyを含む高度な構成もそのまま動作します。ただしDOMは別モジュールとなっており、標準的なdenoテスティングのスコープには含まれていません。

現在、ブラウザ向けアプリケーションは、リクエスト数抑制のため最終的に1ファイルにバンドルする構成が多くあります。deno testはローカルリポジトリにディスクアクセスできるため、バンドルしなくても性能劣化することなくソースコードを直接実行できます。

JavascriptだけでなくTypescriptも直接取り扱えます。
ただしTypescriptコードには型評価を事前実行するため、テスト実行の観点からは概ね阻害要因となります。追加の型評価を回避したい場合には--no-checkオプションを指定するか、あらかじめesbuildなどのコンパイラでJavascriptに変換しておきます。
アサーションによるテストと型評価は異なる手法であるため、同時に実施しなくてはならない理由はないでしょう。

awaitでシリアルに実行可能

APIクライアントのテストは主に非同期処理になりますが、Deno.test()async, awaitをとくに制約なく指定でき、コードの上から順にシリアルに実行できます。

step()メソッドでブロックを記述でき、これもawaitで順次実行できます。
Railsのようなsetup(), teardown()といった暗黙の前後処理の規約はありません。
必要に応じてみずからユーティリティ関数を記述しておき、明示的にテストケースの前後で実行します。

インテグレーションテスト

テストランナーの主要な機能はこれだけですが、外部のAPIをコールするようなインテグレーションテストもとくに支障なく記述できます。

オプショナルなDOMを度外視すると、現在のSPAやWebアプリケーションでよく用いられるAPIクライアントとデータストアを含む結合モジュールをテストできます。
DOMを含める構成は実ブラウザ上の動作チェックと密接に関連するため、deno-dom上のエミュレーションではなく SeleniumによるE2Eテストでカバーする方式が妥当と言えます。

インテグレーションテストでは大規模なライブラリが動く必要があります。たとえばデータストアの標準を確立したReduxなどもそのまま動作するため、API実行後のストア内データをテストするようなシナリオを書けます。

documentオブジェクトを参照するとReferenceErrorで異常終了するため、ライブラリを含めてDOMアクセスするコードを分離しておくことがテスト構成のポイントです。なお、WebStorageAPIは標準機能に含まれているため、localStoragesessionStorageは動作します。

グローバル変数

ES Modulesはファイルスコープであり、テスト実行制御に使えるような動的グローバル変数に規約がありません。標準的な構成ではdomを含んでいないため、docuement.cookieを利用する手法も不適切と言えます。グローバル変数を利用せず、引数にデバッグオプションを追加する設計もあり得ますが、関数がネストするような構成で不利になります。

Denoはwindowをグローバルオブジェクトとして扱うため、テストランナーでwindowのプロパティをセットするとテスト対象コードからも参照できます。 ただし、.ts拡張子のTypeScriptに記述すると、windowの型エラーが発生します。

トップレベルのグローバル変数

関数内でwindowを参照するケースでは、テストケースにセットした値が想定どおりに入りますが、テスト対象コードのトップレベルに記述した変数は、テスト対象コードをロードした時点の値が入るため、意図どおりに動作しないことがあります。

次のようにimport文のかわりにimport()関数を用いると、windowセットアップ後にコードをロードできます。

window.some_flag = 'debug';
const { test_target_function } = await import('../src/test_target_code.js');

このセットアップコードは、import文と異なりテスト内の任意の場所に書けます。Deno.test()内のasync関数内のほか、トップレベルawaitもサポートしています。

npm参照

多くのJavascriptコードがnpmのパッケージを利用しています。Denoは2023年頃からnpm互換性を拡充しており、コード内でnpmパッケージを参照するimportがおおむね動作します。

インストール挙動はdeno testを実行した際に、プロジェクトのpackage.jsonを参照してDeno用のキャッシュをダウンロードします。
パッケージによってはDenoの想定外の構成になっていて、テスト開始前に異常終了すると同時にキャッシュは完了することがあります。この場合、キャッシュがあればそれ以降同一バージョンをダウンロードせずにdeno testが正常動作することもよくあります。

また、deno.json内に Import Mapsを記述すると、別のパスも参照できます。
これを利用して、pnpmなどで別途インストールしたnode_modules/内のローカルパッケージを指定することも可能です。gitリポジトリを指定するケースなどで必要な場合があります。

ただし、Import Mapsを記述していてもDeno用キャッシュのダウンロードは抑制できず、キャッシュ生成時のエラーには直面します。

テストコード用ライブラリはdenoパッケージを利用できる

テストコードはdeno testによる実行に限られるため、npmのほかアプリケーション構成に影響せずdenoパッケージも利用できます。
ミニマムな構成でも、assert()assertEquals()は公式の Assertionモジュールが提供しています。追加の関数は、公式Docsの Assertionsに解説があります。

たとえばPostgreSQLデータベースクライアントとして、 deno-postgresdeno-pgがあります。deno-pgは最新のpgプロトコルに対応しておらず、connect()の段階で異常終了しました。

Deno用パッケージを配布するdeno.landは、import文にURLを記述するだけで実行時に自動セットアップします。プロジェクト内でバージョンを統一したい場合には、既述のImport Mapsを追加定義しておきます。
deno.landは、npmに依存しないDeno用レジストリで、初期から安定動作していました。かつて機能しないDeno向けnpm互換レジストリが存在していたのですが、これはサードパーティが提供する別物でした。

テスト向けツールをdeno.landのパッケージで実装できればpackage.jsondevDependenciesで管理するパッケージを削減できます。

一部のテストケース実行をオプショナルに

deno testはテストケースの definitoin filteringを利用でき、条件に合うテストケースのみ実行できます。

たとえば、Deno.test()ignore: ! Deno.args.includes("--full")というオプションを追加しておくと、deno testのデフォルト挙動ではそのケースをスキップします。
deno test -- --fullと実行することで、このignoreを指定したケースも対象に含まれます。

⁋ 2024/06/25↻ 2024/09/02
中馬崇尋
Chuma Takahiro