ユニットテストを記述する
このセクションでは、これまで作成したCLIアプリケーションにユニットテストを導入します。 ユニットテストの導入と合わせて、ソースコードを整理してテストがしやすくなるようにモジュール化します。
前のセクションまでは、すべての処理をひとつのJavaScriptファイルに記述していました。 ユニットテストを行うためにはテスト対象がモジュールとして分割されていなければいけません。 今回のアプリケーションでは、CLIアプリケーションとしてコマンドライン引数を処理する部分と、MarkdownをHTMLへ変換する部分に分割します。
CommonJSでのモジュール化
実際にアプリケーションのモジュール化をする前に、CommonJSでのモジュール化について簡単に振り返ります。
Node.jsでは、複数のJavaScriptファイル間で変数や関数などをやり取りするために、CommonJSモジュールという仕組みを利用します。
CommonJSモジュールからオブジェクトをエクスポートするには、Node.jsのグローバル変数であるmoduleオブジェクトを利用します。
module.exports
オブジェクトは、そのファイルからエクスポートされるオブジェクトを格納します。
次のgreet.js
というファイルは、greet
関数をエクスポートするモジュールの例です。
greet.js
// greet.js
module.exports = function greet(name) {
return `Hello ${name}!`;
};
require
関数を使って、指定したファイルパスのJavaScriptファイルをモジュールとしてインポートできます。
次のコードでは先ほどのgreet.js
のパスを指定してモジュールとしてインポートして、エクスポートされた関数を取得しています。
greet-main.js
const greet = require("./greet");
greet("World"); // => "Hello World!"
module.exports
オブジェクトそのものに代入するのではなく、module.exports
オブジェクトのプロパティに代入することでも任意の値をエクスポートできます。
次のfunctions.js
というファイルでは、foo
とbar
の2つの関数を同じファイルからエクスポートしています。
functions.js
module.exports.foo = function() {
console.log("foo関数が呼び出されました");
};
module.exports.bar = function() {
console.log("bar関数が呼び出されました");
};
このようにエクスポートされたオブジェクトは、require
関数の戻り値であるオブジェクトのプロパティとしてアクセスできます。
次のコードでは先ほどのfunctions.js
をインポートして取得したオブジェクトからfoo
とbar
関数をプロパティとして取得しています。
functions-main.js
const functions = require("./functions");
functions.foo();
functions.bar();
アプリケーションをモジュールに分割する
それではCLIアプリケーションのソースコードをモジュールに分割してみましょう。
md2html.js
という名前のJavaScriptファイルを作成し、次のようにmarkedを使ったMarkdownの変換処理を記述します。
md2html.js
const marked = require("marked");
module.exports = (markdown, cliOptions) => {
return marked(markdown, {
gfm: cliOptions.gfm,
});
};
このモジュールがエクスポートするのは、与えられたオプションを元にMarkdown文字列をHTMLに変換する関数です。
アプリケーションのエントリーポイントであるmain.js
では、次のようにこのモジュールをインポートして使用します。
main.js
const program = require("commander");
const fs = require("fs");
// md2htmlモジュールをインポートする
const md2html = require("./md2html");
program.option("--gfm", "GFMを有効にする");
program.parse(process.argv);
const filePath = program.args[0];
const cliOptions = {
gfm: false,
...program.opts(),
};
fs.readFile(filePath, { encoding: "utf8" }, (err, file) => {
if (err) {
console.error(err);
process.exit(1);
return;
}
// md2htmlモジュールを使ってHTMLに変換する
const html = md2html(file, cliOptions);
console.log(html);
});
markedパッケージや、そのオプションに関する記述がひとつのmd2html
関数に隠蔽され、main.js
がシンプルになりました。
そしてmd2html.js
はアプリケーションから独立したひとつのモジュールとして切り出され、ユニットテストが可能になりました。
ユニットテスト実行環境を作る
ユニットテストの実行にはさまざまな方法があります。
このセクションではテスティングフレームワークとしてMochaを使って、ユニットテストの実行環境を作成します。
Mochaが提供するテスト実行環境では、グローバルにit
やdescribe
などの関数が定義されます。
it
関数はその内部でエラーが発生したとき、そのテストを失敗として扱います。
つまり、期待する結果と異なるならエラーを投げ、期待どおりならエラーを投げないというテストコードを書くことになります。
今回はNode.jsの標準モジュールのひとつであるassertモジュールから提供されるassert.strictEqual
メソッドを利用します。
assert.strictEqual
メソッドは第一引数と第二引数の評価結果が===
で比較して異なる場合に、例外を投げる関数です。
Mochaによるテスト環境を作るために、まずは次のコマンドでmocha
パッケージをインストールします。
$ npm install --save-dev mocha@7
--save-dev
オプションは、パッケージをdevDependencies
としてインストールするためのものです。
package.json
のdevDependencies
には、そのパッケージを開発するときだけ必要な依存ライブラリを記述します。
ユニットテストを実行するには、Mochaが提供するmocha
コマンドを使います。
Mochaをインストールした後、package.json
のscripts
プロパティを次のように記述します。
{
...
"scripts": {
"test": "mocha test/"
},
...
}
この記述により、npm test
コマンドを実行すると、mocha
コマンドでtest/
ディレクトリにあるテストファイルを実行します。
試しにnpm test
コマンドを実行し、Mochaによるテストが行われることを確認しましょう。
まだテストファイルを作っていないので、Error: No test files found
というエラーが表示されます。
$ npm test
> mocha
Error: No test files found
ユニットテストを記述する
テストの実行環境ができたので、実際にユニットテストを記述します。
Mochaのユニットテストはtest
ディレクトリの中にJavaScriptファイルを配置して記述します。
test/md2html-test.js
ファイルを作成し、md2html.js
に対するユニットテストを次のように記述します。
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const md2html = require("../md2html");
it("converts Markdown to HTML (GFM=false)", () => {
// fs.readFileSyncは同期的にファイルを読み込むメソッド
const sample = fs.readFileSync(path.resolve(__dirname, "./fixtures/sample.md"), { encoding: "utf8" });
const expected = fs.readFileSync(path.resolve(__dirname, "./fixtures/expected.html"), { encoding: "utf8" });
// 末尾の改行の有無の違いを無視するため、変換後のHTMLのスペースをtrimメソッドで削除してから比較しています
assert.strictEqual(md2html(sample, { gfm: false }).trimEnd(), expected.trimEnd());
});
it("converts Markdown to HTML (GFM=true)", () => {
const sample = fs.readFileSync(path.resolve(__dirname, "./fixtures/sample.md"), { encoding: "utf8" });
const expected = fs.readFileSync(path.resolve(__dirname, "./fixtures/expected-gfm.html"), { encoding: "utf8" });
// 末尾の改行の有無の違いを無視するため、変換後のHTMLのスペースをtrimメソッドで削除してから比較しています
assert.strictEqual(md2html(sample, { gfm: true }).trimEnd(), expected.trimEnd());
});
it
関数で定義したユニットテストは、md2html
関数の変換結果が期待するものになっているかをテストしています。
test/fixtures
ディレクトリにはユニットテストで用いるファイルを配置しています。
今回は変換元のMarkdownファイルと、期待する変換結果のHTMLファイルが存在します。
次のように変換元のMarkdownファイルをtest/fixtures/sample.md
に配置します。
test/fixtures/sample.md
# サンプルファイル
これはサンプルです。
https://jsprimer.net/
- サンプル1
- サンプル2
そして、期待する変換結果のHTMLファイルもtest/fixtures
ディレクトリに配置します。
gfm
オプションの有無にあわせて、expected.html
とexpected-gfm.html
の2つを次のように作成しましょう。
test/fixtures/expected.html
<h1 id="サンプルファイル">サンプルファイル</h1>
<p>これはサンプルです。
https://jsprimer.net/</p>
<ul>
<li>サンプル1</li>
<li>サンプル2</li>
</ul>
test/fixtures/expected-gfm.html
<h1 id="サンプルファイル">サンプルファイル</h1>
<p>これはサンプルです。
<a href="https://jsprimer.net/">https://jsprimer.net/</a></p>
<ul>
<li>サンプル1</li>
<li>サンプル2</li>
</ul>
ユニットテストの準備ができたら、もう一度改めてnpm test
コマンドを実行しましょう。2件のテストが通れば成功です。
$ npm test
> mocha
✓ converts Markdown to HTML (GFM=false)
✓ converts Markdown to HTML (GFM=true)
2 passing (31ms)
ユニットテストが通らなかった場合は、次のことを確認してみましょう。
test/fixtures
ディレクトリにsample.md
とexpected.html
、expected-gfm.html
というファイルを作成したか- それぞれのファイルは文字コードがUTF-8で、改行コードがLFになっているか
- それぞれのファイルの末尾に余計な改行文字が入っていないか
なぜユニットテストを行うのか
ユニットテストを実施することには多くの利点があります。 早期にバグが発見できることや、安心してリファクタリングを行えるようになるのはもちろんですが、 ユニットテストが可能な状態を保つこと自体に意味があります。 実際にテストを行わなくてもテストしやすいコードになるよう心がけることが、アプリケーションを適切にモジュール化する指針になります。
またユニットテストには生きたドキュメントとしての側面もあります。 ドキュメントはこまめにメンテナンスされないとすぐに実際のコードと齟齬が生まれてしまいますが、 ユニットテストはそのモジュールが満たすべき仕様を表すドキュメントとして機能します。
ユニットテストの記述は手間がかかるだけのようにも思えますが、 中長期的にアプリケーションをメンテナンスする場合にはかかせないものです。 そしてよいテストを書くためには、日頃からテストを書く習慣をつけておくことが重要です。
まとめ
このユースケースの目標であるNode.jsを使ったCLIアプリケーションの作成と、ユニットテストの導入ができました。
npmを使ったパッケージ管理や外部モジュールの利用、fs
モジュールを使ったファイル操作など、多くの要素が登場しました。
これらはNode.jsアプリケーション開発においてほとんどのユースケースで応用されるものなので、よく理解しておきましょう。
このセクションのチェックリスト
- Markdownの変換処理をCommonJSモジュールとして
md2html.js
に切り出し、main.js
から読み込んだ - mochaパッケージをインストールし、
npm test
コマンドでmocha
コマンドを実行できることを確認した md2html
関数のユニットテストを作成し、テストの実行結果を確認した