【nodejsでユニットテスト】Mocha/ChaiのESModule対応〜ビルドインTest runnerを参考に動作させてみる


※ 当ページには【広告/PR】を含む場合があります。
2023/04/15

Node.jsのユニットテスト界隈で利用されるテスト用フレームワークはいくつか選択肢がありますが、個人的に長年お世話になってきたのは
「Mocha/Chai」です。

参考|Mocha

昨今、Node.jsのCommonJS離れが進み、node18からはESModuleベースのJavascriptコード設計を、開発者側も意識せざるを得ない状況になっています。

と言うことで、元々CommonjsベースのJavascriptコードのユニットテストに使われてきた実績のあるMochaですが、そのままNodejsのバージョンをv18以降に引き上げると、様々なエラーに直面してかなり面喰らってしまうことでしょう。

そんな中、長らくExprerimental扱いであったNodejs純正のユニットテストモジュール・
「Test runner」が、node20から正式にstableになりました。

参考|Test runner - Node.js

こちらは既にESModule対応が充実したユニットテスト機能が充実しているので、現状はストレスなく使うことができます。

Test runnerに関しては、これはこれで素晴らしいことなのですが、過去に作り溜めてきたMocha/Chai用のテストコードの数々を今更容易に捨て去ることもできずにどうすれば...

と困った時にどうにかしてみた時の内容をメモとして残します。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

一旦、純正Test Runnerを使ってみる

Node.jsの新規プロジェクトを作成するような状況であれば、もはやMocha/Chaiをユニットテスト用のフレームワークとしてわざわざ導入する必要性も希薄になってくるやもしれません。

これから一から始めるのであれば
「Test runner」を最初からユニットテストに採用することを検討しても良いでしょう。

ひとまず「Test runner」を使えるNode.jsから、どんなものなのかを軽く試してみます。

Test runnerを使ってみる

「Test runner」はnode18以降でビルドインモジュールとしてそのまま呼び出して利用することができます。

まずは以下のように、何もしない「空振り」をやってみましょう。

            
            $ node --version
v20.10.0

$ node --test
ℹ tests 0
ℹ suites 0
ℹ pass 0
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 110.593313
        
このように使い方は簡単で、例えばテストしたいjsコードを指定することで、

            
            $ node --test hoge.js
        
とすると、この場合hoge.js内に書かれたユニットテストパートが評価されます。

もっと突っ込んで、Typescriptのコードはそのまま使えるのか、何も考えずTest runnerでユニットテストを実行してみると、

            
            $ node --test hoge.ts
node:internal/modules/esm/get_format:160
  throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);
        ^

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /********/hoge.ts
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:160:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:203:36)
    at defaultLoad (node:internal/modules/esm/load:141:22)
    at async ModuleLoader.load (node:internal/modules/esm/loader:409:7)
    at async ModuleLoader.moduleProvider (node:internal/modules/esm/loader:291:45)
    at async link (node:internal/modules/esm/module_job:76:21) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
        
何やらそのままでは、「TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"」というエラー内容の通りで、typescriptコードをトランスパイルできるような変換モジュールの指定が必要のようです。

Test runnerでもTypescriptコードでユニットテストしたい

以下のリンクでも議論されていますが、Test runnerでTypescriptをうまく読み込ませるためのテクニックがいくつか存在しているようです。

参考|How to run the Node built-in testrunner for TypeScript files inside a specific directory?

まずはCommonJS時代からやられている
ts-nodeのライブラリを使った対処法が使えるかチェックしてみます。

            
            $ node --require ts-node/register hoge.ts
TypeError: Unknown file extension ".ts" for /.../hoge.ts
    at Object.getFileProtocolModuleFormat [as file:] (node:internal/modules/esm/get_format:160:9)
    at defaultGetFormat (node:internal/modules/esm/get_format:203:36)
    at defaultLoad (node:internal/modules/esm/load:141:22)
    at async ModuleLoader.load (node:internal/modules/esm/loader:409:7)
    at async ModuleLoader.moduleProvider (node:internal/modules/esm/loader:291:45)
    at async link (node:internal/modules/esm/module_job:76:21) {
  code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
        
なんだかtypescriptファイルをそのまま読み込んでくれる気配がありません...。

と、そこで対処法としてもっとも高評価を受けていた
「tsx」をインストールしてTypescriptローダーとして使うことにします。

参考|TypeScript Execute (tsx): The easiest way to run TypeScript in Node.js

tsxをまだインストールしていなかったので、以下で環境に追加します。

            
            $ yarn add -D tsx
        
これでtsxが使えるようになったので、Test runnerで以下のtsユニットテストを評価してみます。

            
            import assert from 'assert/strict';
import test from 'node:test';

test('1 is equal to 1.', () => {
    assert.strictEqual(1, 1);
});
        
ではユニットテストを実行してみます。

            
            $ node --loader tsx hoge.spec.ts
node:internal/process/esm_loader:40
      internalBinding('errors').triggerUncaughtException(
                                ^
Error: tsx must be loaded with --import instead of --loader
The --loader flag was deprecated in Node v20.6.0 and v18.19.0
    at z (file:///usr/src/app/node_modules/tsx/dist/esm/index.mjs:1:1773)
    at Hooks.addCustomLoader (node:internal/modules/esm/hooks:203:24)
    at Hooks.register (node:internal/modules/esm/hooks:169:16)
    at async initializeHooks (node:internal/modules/esm/utils:233:5)
    at async customizedModuleWorker (node:internal/modules/esm/worker:104:24)
        
またエラーが発生していますが、node20では--loaderオプションが--importオプションという名前に変わったようです。

オプション名を替えて再度Test runnerを叩きます。

            
            $ node --import tsx hoge.spec.ts
✔ 1 is equal to 1. (1.355371ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 13.941092
        
問題なくユニットテストが出来ました。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

Mochaでもtsxをtsローダーとして使ってみる

ではようやく本題ですが、ESModuleベースのNodejsプロジェクトでは、tsコードのトランスパイル後にMochaが正しく機能せずにエラーが起こります。

これは現状でMochaが正常に実行されるのがCommonjsのjavascriptコードを保証しているためで、出力がESModuleになっていると色々と予測できない挙動になります。

「tsx」の良いところは、CJSとESMのコンパイルを内部で良しなに処理してくれるそうで、コード開発者にトランスパイルの処理を意識させることなく使えるように設計されている点にあります。

そこで、先程Test runnerで使ったテクニックをそのままMochaでも利用して、既存のMocha/Chaiベースのユニットテストコードに適用できるか試してみます。

            
            $ mocha --import=tsx test/*.spec.ts
#...ユニットテストの内容は省略
  1 passing (7ms)

Done in 2.46s.
        
手元の環境ではそのままtsxをMochaのtsローダーとして利用することができました。めでたしめでたし。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集

まとめ

今回はMochaとtsxの組み合わせでNodejsのv18以降のESMベースプロジェクトでもそのままユニットテストが通るのか否か即席で試してみました。

大量にあるMocha/Chaiのユニットテストを今更ながら他のユニットテストのフレームワークへ以降させるのはかなり苦労するため、こういった救済措置があるのは助かりました。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート】nodejsをこれから学びたい人のためのオススメ書籍&教材特集