ユニットテストを記述する

このセクションでは、これまで作成したCLIアプリケーションにユニットテストを導入します。 ユニットテストの導入とあわせて、ソースコードを整理してテストがしやすくなるようにモジュール化します。

スクリプトをモジュールに分割する

前のセクションまでは、すべての処理をひとつのJavaScriptファイルに記述していました。 ユニットテストを行うためにはテスト対象がモジュールとして分割されていなければいけません。 今回のアプリケーションでは、CLIアプリケーションとしてコマンドライン引数を処理する部分と、MarkdownをHTMLへ変換する部分に分割します。

Node.jsでは、複数のJavaScriptファイル間で変数や関数などをやりとりするために、モジュールという仕組みを利用します。 モジュールとは変数や関数などを外部にエクスポートするJavaScriptファイルのことです。 モジュールであるJavaScriptファイルは、別のJavaScriptファイルからインポートできます。 モジュールからオブジェクトをエクスポートするには、Node.jsのグローバル変数であるmoduleオブジェクトを利用します。 module.exportsオブジェクトは、そのファイルからエクスポートされるオブジェクトを格納します。 次のコードは簡単な関数をエクスポートするモジュールの例です。

// greet.js
module.exports = function greet(name) {
    return `Hello ${name}!`;
};

require関数は別のJavaScriptファイルをモジュールとしてインポートできます。 次の例では先ほどのモジュールをインポートして、エクスポートされた関数を取得しています。

const greet = require("./greet");
greet("World"); // => "Hello World!"

module.exportsオブジェクトに直接代入するのではなく、そのプロパティとして任意の値をエクスポートできます。 次の例では2つの関数を同じファイルからエクスポートしています。

// functions.js
module.exports.foo = function() { /**/ };
module.exports.bar = function() { /**/ };

このようにエクスポートされたオブジェクトは、require関数の戻り値のプロパティとしてアクセス可能になります。

const functions = require("./functions");
functions.foo();
functions.bar();

それではCLIアプリケーションのソースコードをモジュールに分割してみましょう。 md2html.jsという名前のJavaScriptファイルを作成し、次のようにMarkdownの変換処理を記述します。

const marked = require("marked");

module.exports = (markdown, options = {}) => {
    const markedOptions = {
        gfm: false,
        sanitize: false,
        ...options,
    };

    return marked(markdown, {
        gfm: markedOptions.gfm,
        sanitize: markedOptions.sanitize
    });
};

このモジュールがエクスポートするのは、与えられたオプションをもとにMarkdown文字列をHTMLに変換する関数です。 アプリケーションのエントリポイントであるmain.jsでは、次のようにこのモジュールをインポートして使用します。

const program = require("commander");
const fs = require("fs");
const md2html = require("./md2html");

program
    .option("--gfm", "GFMを有効にする")
    .option("-S, --sanitize", "サニタイズを行う");

program.parse(process.argv);
const filePath = program.args[0];

fs.readFile(filePath, "utf8", (err, file) => {
    if (err) {
        console.error(err);
        process.exit(err.code);
        return;
    }
    const html = md2html(file, program.opts());
    console.log(html);
});

markedパッケージや、そのオプションに関する記述がひとつのmd2html関数に隠蔽され、main.jsがシンプルになりました。 そしてmd2html.jsはアプリケーションから独立したひとつのモジュールとして切り出され、ユニットテストが可能になりました。

ユニットテスト実行環境を作る

ユニットテストの実行にはさまざまな方法がありますが、 このセクションではテスティングフレームワークとしてMochaを使って、ユニットテストの実行環境を作成します。 Mochaが提供するテスト実行環境では、グローバルにitdescribeなどの関数が定義されます。 it関数はその内部でエラーが発生したとき、そのテストを失敗として扱います。 つまり、期待する結果と異なるならエラーを投げ、期待どおりならエラーを投げないというテストコードを書くことになります。

テストコード中でエラーを投げるために、今回はNode.jsの標準モジュールのひとつであるassertモジュールから提供されるassert関数を利用します。 assert関数は引数の評価結果がfalseであるとき、実行時にエラーを投げます。

Mochaによるテスト環境を作るために、まずは次のコマンドでmochaパッケージをインストールします。

$ npm install --save-dev mocha@6

--save-devオプションは、パッケージをdevDependenciesとしてインストールするためのものです。 package.jsonのdevDependenciesには、そのパッケージを開発するときだけ必要な依存ライブラリを記述します。

ユニットテストを実行するには、Mochaが提供するmochaコマンドを使います。 Mochaをインストールした後、package.jsonのscriptsプロパティを次のように記述します。

{
    ...
    "scripts": {
        "test": "mocha"
    },
    ...
}

この記述により、npm testコマンドを実行したときにmochaコマンドが呼び出されます。 試しに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", () => {
    const sample = fs.readFileSync(path.resolve(__dirname, "./fixtures/sample.md"), "utf8");
    const expected = fs.readFileSync(path.resolve(__dirname, "./fixtures/expected.html"), "utf8");

    assert(md2html(sample) === expected);
});

it関数で定義したユニットテストは、md2html関数の変換結果が期待するものになっているかをテストしています。 test/fixturesディレクトリにはユニットテストで用いるファイルを配置しています。 今回は変換元のMarkdownファイルと、期待する変換結果のHTMLファイルの2つが存在します。

ユニットテストを記述したら、もう一度改めてnpm testコマンドを実行しましょう。1件のテストが通れば成功です。

$ npm test
> mocha

  ✓ converts Markdown to HTML

  1 passing (18ms)

なぜユニットテストをおこなうのか

ユニットテストを実施することには多くの利点があります。 早期にバグが発見できることや、安心してリファクタリングをおこなえるようになるのはもちろんですが、 ユニットテストが可能な状態を保つこと自体に意味があります。 実際にテストをおこなわなくてもテストしやすいコードになるよう心がけることが、アプリケーションを適切にモジュール化する指針になります。

またユニットテストには生きたドキュメントとしての側面もあります。 ドキュメントはこまめにメンテナンスされないとすぐに実際のコードと齟齬が生まれてしまいますが、 ユニットテストはそのモジュールが満たすべき仕様を表すドキュメントとして機能します。

ユニットテストの記述は手間がかかるだけのようにも思えますが、 中長期的にアプリケーションをメンテナンスする場合にはかかせないものです。 そしてよいテストを書くためには、日頃からテストを書く習慣をつけておくことが重要です。

まとめ

このユースケースの目標であるNode.jsを使ったCLIアプリケーションの作成と、ユニットテストの導入ができました。 npmを使ったパッケージ管理や外部モジュールの利用、fsモジュールを使ったファイル操作など、多くの要素が登場しました。 これらはNode.jsアプリケーション開発においてほとんどのユースケースで応用されるものなので、よく理解しておきましょう。