Express で構築した WebAPI サーバをユニットテストする (コードカバレッジも見る)

Express で構築した WebAPI サーバをユニットテストしたく、やり方を調べた。

目次

今回の要件

今回の前提、および達成したいことは以下のとおり。

Angular を触っていたので、Angular CLI による雛形プロジェクトが内包する、「Jasmine + Karma + Istanbul」な構成で、WebAPI サーバプロジェクトをテストしたいな、と思っている。

テストランナーは Mocha を使う

Angular アプリの場合は、テストを書くライブラリとして Jasmine があり、それを実行するテストランナーとして Karma が存在した。だが、Karma はブラウザ上でテストを行うテストランナーで、今回のように画面を持たない WebAPI サーバのユニットテストにはあまり適さないのだ。ということで Jasmine + Karma な構成は断念。

他のツールを調べてみると、Mocha というフレームワークを使う例が多かったので、コレを採用することにした。karma.conf.js のような設定ファイルを作ることなく、ゼロコンフィグでユニットテスト環境が作れる。

# mocha をインストールする
$ npm install mocha --save-dev

# mocha はデフォルトでプロジェクト直下の test/ ディレクトリ内にある .js ファイルをテストコードとして認識するので
# まずは test/ ディレクトリを作る
$ mkdir test

# test/ ディレクトリ配下にテストコードを書くファイルを1つ試しに作る
$ touch test/my-test.js

test/my-test.js の中身はこんな風に、describe()it() でテストケースを別けて書ける。Jasmine っぽい。

describe('HOGE', () => {
  it('FUGA', (done) => {
    if('aaa' === 'aaa') {
      done();
    }
    else {
      done('失敗');
    }
  });
});

まずはテストファイルのみで、何の外部ファイルも読み込んでいないが、コレでテストが動くか確認してみる。

# ローカルインストールした mocha を実行するため npx を使用
$ npx mocha

コレでいきなりテストが実行され、コンソール上に結果が表示されるはずだ。終了する際は Ctrl + C を押さないといけない様子。

Express サーバの制御には supertest を使う

次に、Express サーバを構成するコードを読み込んでテストをしたいので、Express サーバを制御するのに使える supertest という npm パッケージを使ってみる。

$ npm install supertest --save-dev

アプリ側のコードは以下のようにしておく。

const express = require('express');

const app = express();

// ルーティング定義などなど……
app.use('/', require('./routes/router'));

// UT 時はココが実行されないようにしておく
if(!module.parent) {
  // サーバ起動
  const port = process.env.PORT || 8080;
  app.listen(port, () => {
    console.log(`Listen : ${port}`);
  });
}

// 変数 app をエクスポートしておく
module.exports = app;

で、テストコード側は以下のように書く。

// 前述の Express サーバのコード
const app = require('../src/index');

// ココで listen() する
const supertest = require('supertest').agent(app.listen());

describe(`GET : '/'`, () => {
  it('200 が返されるべきである', (done) => {
    supertest.get('/')
      .expect(200)
      .end((error, response) => {
        if(error) {
          return done(error);
        }
        done();
      });
  });
});

アプリ側のコードで if(!module.parent) と判定している部分が見慣れないだろうか。コレは、ユニットテストコード内で supertest が複数回 app.listen() してしまうのを避けるために仕込んでいる。

この関係で、アプリ側のコードの if(!module.parent) ブロック内はコードカバレッジが通らなくなることに留意。まぁココはサーバ起動のためのコードだからいいでしょ。w

とりあえずコレでテストが書けるようになった。

HTML 形式でレポートを出力するために nyc を使う

続いて、テスト結果を Istanbul の HTML 形式で出力したいので、nyc というパッケージを利用する。コレは Mocha の動作結果をパイプして、Istanbul レポータを出力したり仲介してくれるツール。

コレをローカルインストールし、mocha コマンドの手前に挟む。

$ npm install nyc --save-dev

package.json で以下のように npm-scripts を組む。

"scripts": {
  "test": "nyc --reporter=text --reporter=html mocha --watch"
}

このようにして、$ npm test を実行すると、基本は mocha --watch のとおり、ファイルの変更を監視する Watch モードで動作し、ひととおりテストが終わると、nyc が HTML 形式のレポートを ./coverage/ ディレクトリに出力してくれる。また、Ctrl + C で終了すると、nyc がコンソールにレポートを出力してくれる。

任意 : Jasmine 風にテストを書けるように expect をインストールする

ココまででやりたいことは達成できたが、テストを書く時の比較検証を Jasmine 風に書きたいので、expect というツールを入れることにする。

$ npm install expect --save-dev

そしてテストコードを以下のように修正する。

// Jasmine 風な expect() を書くため入れる
const expect = require('expect');

// Express サーバのコード
const app = require('../src/index');

// ココで listen() する
const supertest = require('supertest').agent(app.listen());

describe(`GET : '/'`, () => {
  it('200 が返されるべきである', (done) => {
    supertest.get('/')
      .expect(200)
      .end((error, response) => {
        if(error) {
          return done(error);
        }
        // Jasmine 風に expect() が書けるようになる
        expect(response.text).toBe('Hello World');
        done();
      });
  });
});

こんな感じで expect().toBe() とかが書けるようになった。楽チン。

以上

ということで、

という構成で解決できた。

今回作ったサンプルコードを含む、実際に動作するサンプルプロジェクトを以下に作ったので、コチラも参考にしていただければ幸い。