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は標準機能に含まれているため、localStorage
やsessionStorage
は動作します。
グローバル変数
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-postgresや
deno-pgがあります。deno-pgは最新のpgプロトコルに対応しておらず、connect()
の段階で異常終了しました。
Deno用パッケージを配布するdeno.landは、import
文にURLを記述するだけで実行時に自動セットアップします。プロジェクト内でバージョンを統一したい場合には、既述のImport Mapsを追加定義しておきます。
deno.landは、npmに依存しないDeno用レジストリで、初期から安定動作していました。かつて機能しないDeno向けnpm互換レジストリが存在していたのですが、これはサードパーティが提供する別物でした。
テスト向けツールをdeno.landのパッケージで実装できればpackage.json
のdevDependencies
で管理するパッケージを削減できます。
一部のテストケース実行をオプショナルに
deno test
はテストケースの
definitoin filteringを利用でき、条件に合うテストケースのみ実行できます。
たとえば、Deno.test()
にignore: ! Deno.args.includes("--full")
というオプションを追加しておくと、deno test
のデフォルト挙動ではそのケースをスキップします。
deno test -- --full
と実行することで、このignore
を指定したケースも対象に含まれます。
Chuma Takahiro