Nodeアプリケーションの機能テスト
Web開発プロジェクトで機能のテストをするには、起こり得るユーザーの操作リストを基にブラウザーでDOMの遷移をチェックします。仮にコンテンツマネジメントシステム(CMS)を作っているとして、画像ライブラリーのアップロード機能をテストするなら、実際に画像をアップロードし、画像が追加されたことと一覧に反映されたかを確認します。
Nodeアプリの機能テストツールは「ヘッドレス」と「ブラウザーベース」2つの方法があります。画面表示などのユーザーとの接点がないヘッドレスは、通常PhantomJSのようなツールでターミナルからの操作に向いたブラウザーを使い、大がかりでない場合は、CheerioやJSDOMなどのライブラリーを使います。ブラウザーベースのテストは、ブラウザーの操作を自動化するSeleniumのようなツールにスクリプトを書いて本物のブラウザーを使います。どちらの方法もMocha、Jasmine、Seleniumを動かすCucumberと、同じNodeのテストツールが使えます。
Selenium
SeleniumはJavaをベースとしたブラウザー自動化用ライブラリーで、Nodeアプリのテストに使えます。言語特有のドライバーでSeleniumのサーバーに接続し、本物のブラウザーでテストできるのです。Node SeleniumドライバーWebdriverIOの使い方を解説します。
Seleniumは、JavaのインストールとSelenium JARファイルのダウンロードが必要なので、純粋なNodeのテスト用ライブラリーよりも少し大変です。OSに合ったJavaをダウンロードして、SeleniumのサイトでJARファイルをダウンロードします。続いてSeleniumのサーバーを立ち上げます。
java -jar selenium-server-standalone-3.4.0.jar
Seleniumのバージョンが異なるかもしれません。
使用しているブラウザーのバイナリファイルへのパスを入力します。たとえばWindows10でFirefoxを使っているなら、browserNameにセットし、Firefoxへの完全なパスを特定ます。
java -jar -Dwebdriver.firefox.driver="C:\path\to\firefox.exe" selenium-server-standalone-3.4.0.jar
もしくはmozillaのGeckoドライバーをダウンロードし、Seleniumの実行ファイルと同じフォルダーに置いて起動します。
java -jar -Dwebdriver.gecko.driver=geckodriver selenium-server-standalone-3.4.0.jar
正確なパスはFirefoxのインストール先により異なります。FirefoxドライバーはSeleniumHQのドキュメントを参照してください。同じくChromeやMicrosoftのEdgeブラウザー用のドライバーもあり、同様に設定できます。
Seleniumサーバーが立ち上がったので、新規Nodeプロジェクトを作成し、WebdriverIOをインストールします。
mkdir -p selenium/test/specs
cd selenium
npm init -y
npm install --save-dev webdriverio
npm install --save express
WebdriverIOは、コンフィグファイルを生成する便利な機能を備えています。実行するにはwdio configを実行します。
./node_modules/.bin/wdio config
並んでいる「?」の項目を確認しつつ、初期値を使います。以下のようになっているはずです。
wdioコマンドでpackage.jsonファイルをアップデートして、npm testで実行できるようにします。
"scripts": {
"test": "wdio wdio.conf.js"
},
テストになにか加えましょう。基本的なExpressサーバーで十分です。例ではテスト実行のための手順を書きます。以下のコードをindex.jsで保存します。
const express = require('express');
const app = express();
const port = process.env.PORT || 4000;
app.get('/', (req, res) => {
res.send(`
<html>
<head>
<title>My to-do list</title>
</head>
<body>
<h1>Welcome to my awesome to-do list</h1>
</body>
</html>
`);
});
app.listen(port, () => {
console.log('Running on port', port);
});
WebdriverIOは、Seleniumのテストを記述するためのシンプルかつスムーズなAPIが用意されています。構文は単純で学びやすく、テストの記述にCSSセレクタを使えます。次のコード(test/specs/todo-test.js)は、WebdriverIOクライアントをセットアップして、ページのタイトルを確認する単純なテストです。
const assert = require('assert');
const webdriverio = require('webdriverio');
describe('todo tests', () => {
let client;
before(() => {
client = webdriverio.remote();
return client.init();
});
it('todo list test', () => {
return client
.url('http://localhost:4000')
.getTitle()
.then(title => assert.equal(title, 'My to-do list'));
});
});
WebdriverIOに接続できたら、テスト対象となるサイトからページを取得するクライアントのインスタンスが生成できます。ブラウザーのオブジェクトの状態が参照できます。例ではgetTitleでヘッダー内からタイトルを取得しました。ページのCSS要素を参照するには、.elementsを使います。フォームや既存のクッキーまで、ページの要素を操作するメソッドが用意されています。
Node Webアプリに対して実際のブラウザーからテストしましょう。ポート4000でサーバーを開始します。
PORT=4000 node index.js
npm testを実行します。Firefoxが開き、コマンドライン上でテストが実行されます。Chromeを使いたい場合は、wdio.conf.jsを開いてbrowserNameプロパティを変更してください。
Seleniumのさらに高度なテスト方法
WebdriverIOとSeleniumを使って、ReactやAngularで作られた複雑なWebアプリのテストをするなら、ユーティリティ・メソッドがポイントです。いくつかのメソッドはある要素の準備ができるまでテストを停止しているので、Reactアプリのようにページを非同期通信でレンダリングし、外部データに応じて何度も更新する場合に重宝します。詳しくはメソッド名にwaitFor*がついたもの、たとえばwaitForVisibleを確認してください。
テスト手法は、まだ手動でやってない? UIテストを爆速で自動化できるNightwatch.jsが便利すぎが参考になります。
テスト失敗時の対処方法
完成したプロジェクトに取り組むなかで、テストに失敗することがあります。Nodeではテスト失敗時、さらに詳細を取得するためのツールがいくつかあります。テスト失敗後のデバッグ作業のために、出力結果を詳しくする方法として、NODE_DEBUGを説明します。
もっと詳しいログデータを得る
テスト失敗時、プログラムがなにをしているのか詳細な情報が役に立ちます。Nodeで詳しいログデータをとるには2つの方法があります。1つはNodeの内部用、もう1つはnpmモジュール用です。NodeのコアモジュールのデバッグならNODE_DEBUGを使います。
NODE_DEBUGを使う
NODE_DEBUGの機能を説明するため、何重もの入れ子になったファイルシステムのコールでコールバックを書き忘れたバグを想定します。例外を投げてテストします。
const fs = require('fs');
function deeplyNested() {
fs.readFile('/');
}
deeplyNested();
スタックトレースでは限られた情報を示します。例外が発生した場所の詳しい情報はありません。
fs.js:60
throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs
^
Error: EISDIR: illegal operation on a directory, read
at Error (native)
プログラマーは詳細のコメントがないトレース結果を見て、不親切なエラー表示だとNodeを罵ります。しかし、上記のコメントにある通り、fsモジュールの詳しい情報はNODE_DEBUG=fsで得られます。以下のスクリプトを実行してください。
NODE_DEBUG=fs node node-debug-example.js
デバッグに役立つ詳細情報が書かれたトレース結果を得られます。
fs.js:53
throw backtrace;
^
Error: EISDIR: illegal operation on a directory, read
at rethrow (fs.js:48:21)
at maybeCallback (fs.js:66:42)
at Object.fs.readFile (fs.js:227:18)
at deeplyNested (node-debug-example.js:4:6)
at Object.<anonymous> (node-debug-example.js:7:1)
at Module._compile (module.js:435:26)
at Object.Module._extensions..js (module.js:442:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:311:12)
at Function.Module.runMain (module.js:467:10)
このトレース結果なら、7行目から呼ばれた4行目の関数に問題があると分かります。コアモジュールを使うコードのデバッグが格段に楽になります。ファイルシステムだけでなくNode HTTPクライアント&サーバーモジュールをはじめ、ネットワークライブラリーも含まれます。
DEBUGを使う
公開されているNODE_DEBUGの代替ツールは、DEBUGです。npmの多くのパッケージはDEBUGの環境変数を参照します。NODE_DEBUGのパラメーター形式を模していて、対象を指定するかDEBUG='*'でモジュール全部をデバッグできます。
プロジェクトでNODE_DEBUGを組み込むなら、ビルトインで用意されたutil.debuglogメソッドを使ってください。
const debuglog = require('util').debuglog('example');
debuglog('You can only see these messages by setting NODE_DEBUG=example!');
DEBUG用にカスタムしたデバッグロガーを作るには、npmのデバッグパッケージがオススメです(https://www.npmjs.com/package/debug)。必要なだけロガーを作成できます。MVC(Model、View、Controller)構造のWebアプリなら、モデル用、ビュー用、コントローラー用に別々のロガーを作成でき、テストに失敗したときには、アプリの特定箇所のデバッグに最適なログを参照できます。以下のコードはデバッグモジュールの使い方を示しています。
const debugViews = require('debug')('debug-example:views');
const debugModels = require('debug')('debug-example:models');
debugViews('Example view message');
debugModels('Example model message');
サンプルを実行してログを見るにはDEBUGをdebug-example:viewsにセットし、DEBUG=debug-example:views node index.jsとします。
デバッグモジュールのロギング機能の1つで、デバッグ範囲の先頭にハイフン(-記号)をつけた部分をログから削除できます。
DEBUG='* -debug-example:views' node index.js
特定のモジュールを隠せるので、たとえばワイルドカード(*記号)ですべてを対象にしながら、不要な項目だけ除外できるのです。
さらに優れたスタックトレース結果を得る
非同期処理かつ非同期コールバックかプロミスを使っているなら、スタックトレースで詳しい情報が得られないと困ります。npmパッケージには対策があります。たとえばコールバックが非同期で実行されるときに関数の順番待ちが発生してもNodeはコールスタックを保持しません。試してみましょう。非同期処理の関数を定義したasync.jsと、async.jsを読み込むindex.js、合計2つのファイルを作ります。
次のコードはaync.jsのものです。
module.exports = () => {
setTimeout(() => {
throw new Error();
})
};
index.js内でasync.jsを要求します:
require('./async.js')();
node index.jsにより、index.jsを実行すると短いスタックトレースが得られます。エラーのあった関数の呼び出し元はなく、例外が投げられた場所だけを示します。
throw new Error();
^
Error
at null._onTimeout (async.js:3:11)
at Timer.listOnTimeout (timers.js:92:15)
改善します。trace packageをインストールして、node -r trace index.jsで実行します。-rフラグはNodeに対し、ほかをロードする前にまずこのトレースモジュールを先にロードするよう要求しているのです。
スタックトレースに関するほかの問題に、情報が多すぎる場合があります。トレース結果がNodeの内部についての大量の情報を含む場合に起こります。スタックトレースをすっきりさせるにはclarifyを使います。実行には-rフラグを付けます:
$ node -r clarify index.js
throw new Error();
^
Error
at null._onTimeout (async.js:3:11)
Webアプリのエラーを警報する自動で配信されるEメール本文にスタックトレースの結果を含めるときも、Clarifyは役に立ちます。
Nodeでブラウザー向けのコードを実行するなら、クライアントとサーバー側で同じコードを実行するWebアプリ(isomorphic web application)だと思いますが、source-map-supportを使うと、より良いスタックトレースの結果が得られます。-rを付けて実行できますが、いくつかほかのテストフレームワーク上で実行することもできます。
node -r source-map-support/register index.js
mocha --require source-map-support/register index.js
非同期処理コードのスタックトレースの結果と格闘することがあれば、traceやclarifyなどのツールを活用し、V8とNodeの機能を最大限活かしてください。
最後に
Seleniumを使ったNodeアプリの機能テストの方法を解説しながら、テスト失敗時のヒントについても解説しました。
本記事は著書『Node.js in Action, Second Edition』(マニング社)の抜粋です。内容が一新された第2版では、本番で通用する品質のNodeアプリ開発に必要な機能、テクニック、考え方をすべて網羅しています。
(原文:A Guide to Testing and Debugging Node Applications)
[翻訳:西尾 健史/編集:Livit]