January 2016

Volume 31 Number 1

働くプログラマ - MEAN あれこれ: MEAN のテスト

Ted Neward | January 2016

Ted Neward「ノーディスト」の皆さん、こんにちは。入力、出力、データベース、ユーザー、UI、API、そしてパラレル ワールドについての連載の続編へようこそ (まだお読みでなければ、第 1 回 msdn.com/magazine/mt185576 からご覧ください)。

前回 (msdn.com/magazine/mt595757) は、アプリケーションで API エンドポイント (非常に簡単な「人物」のデータベース) を構築し、今まで R だけだった CRUD 機能に CUD を含めました。コツコツとした積み重ねも確かに重要ですが、変更点を確認でき、その変更点について問い合わせれば結果が返ると便利です。

ここで問題が持ち上がります。今回のような Web API をビルドしている場合、開発者が Web ページを起動し、何かが正しく機能していることを目視確認できる「簡単なテスト」は利用できません。確か、前回指摘したように、cURL、Postman、Runscope などのツールを使用すれば簡単に目視確認できます。ですが、実は、目視確認が重要なわけではありません。すべてがスムーズかつ正しく実行していることを確認するには、なんらかの自動テストが必要です。したがって、実行しやすく、コードを適切にカバーするテスト スイートが必要です。テスト スイートのこれ以外の特徴は、多くのドキュメントで適切に解説されています。

詳細に入る前に、注意事項として、「単体」テストについて触れておきます。単体テストとは、コードの内部動作を詳しく検査するもので、通常はある程度のモックの作成 (このコラムで言えば、たとえば Express の要求オブジェクトと応答オブジェクト) が必要になります。単体テストは、小規模で対象を限定してすばやく実行するものなので、プログラマの手を煩わせて思考を止めることなく、(C# のようなコンパイル言語でのコンパイル直後に) ビルド プロセスの一環として使用します。一方「統合」テストや「機能」テストはもっと「外的な」テストです。つまり、テストにフレームワークを利用し、層を不明瞭なエンティティとして扱います。Web API をテストする場合、個人的には後者が好みです。Express コントローラーの単体テストの作成を試みると、モックの作成にたいへん手間がかかり、そのデメリットが他のメリットを上回るためです。一般に、コントローラーは 1 つの目的で作成されていること、そして、自作コードのテストを阻害するバグが Express 層にはほとんどなかったことを踏まえ、労力に見合う価値があるかどうかという点で外部テストの方が適切だと判断しました。

横道にそれましたが、ここからは、前回までにビルドした人物 API のテストを作成します。そのために使用する Node.js パッケージとして、すぐに思い浮かんだのが mocha と supertest です。

テストの実行

もちろん、Node なので、まずは npm から始めます。今回は、mocha と supertest の両方を npm install を使ってプロジェクトに追加する必要があります。

npm install --save-dev mocha supertest

(別々に実行するのが昔からのやり方ですが、そうしなければならない理由はありません。両方使用することがわかっている場合は特に、まとめて実行しない手はありません。)

先に進む前に、--save-dev 引数について触れておきます。これにより、mocha と supertest は package.json ファイルの devDependencies セクションに配置されます。つまり、これらは開発時のみの依存関係になり、クラウド ホスト (Microsoft Azure など) が運用環境で実行するコードを準備するときには除外されます。

Node.js コミュニティは、プロジェクトの他の部分とほぼ同様に、テスト フレームワークを小さな単位として扱います。そのため、開発者は自身が定めた方法でこうした単位にまとめ、そこから全体を組み立てることができるため、開発者にとってはおそらくありがたいしくみです。今回のテストは、mocha パッケージから着手します。mocha の公式サイトによれば、「非同期テストをシンプルかつ楽しく行う」ために設計されたテスト パッケージだそうです。このパッケージは、開発者が希望すれば、テスト作業の基本的な「スキャフォールディング」として機能します。しかし、今回のテスト対象は Web API なので (また、低レベルの HTTP コードを何もかも作成したくないので)、2 つ目のライブラリ、supertest を使用します。このライブラリは、いくつか便利な HTTP 固有のテスト動作を追加します。この 2 つに加えて、should というオプションのライブラリも使用します。というのも、ビヘイビアー駆動開発 (BDD) スタイルの should アサーションが好みだからです。ですが、従来からのアサーション スタイルがお好みであれば、mocha 付属の assert パッケージもあります。

両方のパッケージをインストールしたら、テストを始めるために、当然ながら最初のテストを作成します。必須ではありませんが、Node.js プロジェクトでは、テスト コードを置く別のサブディレクトリ (ここでは「test」) を作成するのが一般的です。このディレクトリに、1 つ目のテスト ファイル getTest.js を置きます (図 1 参照)。

図 1 一例としていくつかベースライン テストをセットアップ

var supertest = require('supertest');
  var express = require('express');
  var assert = require('assert');
  var should = require('should');
  describe('Baseline tests', function() {
    describe('#indexOf()', function () {
      it('should return -1 when the value is not present', function () {
        assert.equal(-1, [1,2,3].indexOf(5));
        assert.equal(-1, [1,2,3].indexOf(0));
        [1,2,3].indexOf(5).should.equal(-1);
        [1,2,3].indexOf(0).should.equal(-1);
      });
    });
  });

先に進む前に、いくつか説明しておくことがあります。describe メソッドは、出力と実行の単純なペアになっています。文字列はテスト コンソール (通常はコマンド プロンプトやターミナルのウィンドウ) に表示するメッセージ、function はテストとして実行する内容です。describe ブロックは入れ子になっています。これは、論理的な行に沿ってテストを分割するのに便利です。このようにすると、必要であればその後に skip 関数を使用して、ブロック全体を無効にすることができます。it 関数は 1 つのテストを表します。今回は、JavaScript 配列の indexOf メソッド関数が想定どおりに動作していることをテストします。「it」に渡す文字列はここでもテスト コンソールに表示するテストの説明です。

it 呼び出しに入れ子にしている関数が実際のテストです。ここでは、従来型のアサーション assert と、BDD 型アサーション should の両方を使ってみました。後者の方が、より Fluent な API になります。実は、他にも選択肢はありますが (mocha の公式ページでは、他にも 2 個のアサーションを紹介しています)、個人的に should が好みなので、これを使用することにします。ただし、assert は mocha と supertest と一緒にインストールされますが、should パッケージは npm を使ってインストールする必要があります。

簡単なテストを実行するため、app.js を含むディレクトリに「mocha」と入力すると、test サブディレクトリからテストを自動的に選択します。ただし、Node.js にはちょっと便利な使い方があります。これは、npm へのコマンドライン パラメーターのセットを含めるために、package.json ファイルにスクリプトのコレクションを含める方法です。その結果、「npm start」ではサーバーを起動できます。「npm test」では、管理者 (またはクラウド システム) はコードのビルドに使用されたものを知らなくても、テストを実行できます。そのためには、以下のように、package.json 内の「scripts」コレクションを適宜入力します。

{
  "name": "MSDN-MEAN",
  "version": "0.0.1",
  "description": "Testing out building a MEAN app from scratch",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "test": "mocha"
  },
  // ...
}

これで、「npm test」と入力するだけで、テストが起動され、indexOf が正しく機能していることを突き止めます。

次に、これらのテストを無効にしましょう (もちろん、完全に削除することもできますが、セットアップで何かエラーが発生した場合に備えて、機能することがわかっているベースラインを保持するようにしています)。以下のように、連鎖する describe の一部として skip を使用するだけです。

describe.skip('Baseline tests', function() {
  describe('#indexOf()', function () {
    it('should return -1 when the value is not present', function () {
      assert.equal(-1, [1,2,3].indexOf(5));
      assert.equal(-1, [1,2,3].indexOf(0));
      [1,2,3].indexOf(5).should.equal(-1);
      [1,2,3].indexOf(0).should.equal(-1);
    });
  });
});

これで、テストがスキップされるようになります。テスト コンソールには残っていますが、テストが実行されたことを示すチェックマークが付いていません。

API のテスト

さて、indexOf は十分にテストしましたが、GET /persons エンドポイントについてはこれからです。

ここで supertest の出番です。supertest を使用すれば、エンドポイント (今回は、前述のアプリ コードによって確立した localhost とポート 3000 のサーバー設定を使用) をクエリして、その結果を検証できる、小さな HTTP エージェントを作成できます。したがって、いくつか追加するものがあります。まず、アプリ コードをテストの一環として読み込んで、実行する必要があるため、次に示す、テストの 1 行目にある親ディレクトリの app.js ファイルが必要になります。

require("../app.js")

これにより、アプリが起動して (このことは DEBUG 環境変数を app に設定しておけば見分けやすくなります) 実行されます。

次に、supertest ライブラリを使用し、ローカル ホスト サーバーとポート 3000 に対して要求オブジェクトを作成します。これにより、その要求オブジェクトを使用して、person ID が 1 の /persons エンドポイントに対して要求を発行することができます (/persons/1 のようにすることを思い出してください)。その後、適切な結果 (200 状態コード) が戻ってくること、JSON として戻ってくること、および JSON 本文に想定どおりのものが含まれていることを検証できます (図 2 参照)。

図 2 あらゆる人物の中を検索

var request = supertest("http://localhost:3000");
describe('/person tests', function() {
  it('should return Ted for id 1', function(done) {
    request
      .get('/persons/1')
      .expect(200)
      .expect('Content-Type', /json/)
      .expect(function(res) {
        res.body.id.should.equal(1)
        res.body.firstName.should.equal("Ted")
        res.body.lastName.should.equal("Neward")
        res.body.status.should.equal("MEANing")
      })
      .end(done);
    });
});

Express と同様の verb メソッド方式で、要求オブジェクトに固有の Fluent API が使用されています。これにより、get('/persons/1') がその URL に対する GET 要求に変換されます。ただし、これに続く expect メソッドは、特定の種類の結果を想定し、その結果が戻らなければ、テスト全体を失敗させます。重要なのが、最後の end メソッドです。このメソッドは、it にパラメータとして渡した done オブジェクトを利用します。done オブジェクトは、テストが完了したことを通知するコールバックです。supertest はこのテストをすべて、順次非同期に実行 (Node.js 方式) するため、テストが完了したという通知が必要になります。done を使用しないと、各テストの 2 秒後、supertest はテストが時間切れになったと見なしてエラーを通知します。

最後に、スペースの都合上、データベースに人物を追加できることを検証することにします。このためには、post メソッドを使用して、システムに追加する人物のインスタンスを含む新しい JSON オブジェクトを送信します (図 3 参照)。

図 3 人物を追加できることを検証

it('should allow me to add a person', function(done) {
  request
    .post('/persons')
    .send({'firstName':'Ted', 'lastName':'Pattison','status':'SharePointing'})
    .expect(200)
    .expect('Content-Type',/json/)
    .expect(function(res) {
      should.exist(res.body.id);
      res.body.firstName.should.equal("Ted");
      res.body.lastName.should.equal("Pattison");
    })
    .end(done);
  })

簡単だと思いませんか。本体を検証する expect の最初の行は、id フィールドの存在をテストします。ただし、should ライブラリは、id フィールドが未定義状態にならないことを信頼できません。未定義状態でメソッドまたはプロパティを呼び出すと例外が生成されるため、id フィールドの存在を検証するために should を直接使用する必要があります。それを除けば、適切に解釈されます。

テスト対象のその他 2 つのメソッド、PUT と DELETE については、実に簡単にテストを作成できます。PUT エンドポイントのテストでは、post の代わりに put を使用します。URL は 1 人の人物の URL (たとえば、"Ted" "Neward" という人物の /persons/1) をポイントする必要があります。送信する JSON 本文は、POST の場合と同じに見えますが、値が異なります。テストは、更新が受け取られるかどうかを確かめることが目的だからです。その後、返された JSON 本文が更新を反映していることをテストします (返される新しいオブジェクトで更新が反映されるかどうかも確かめるため)。一方、DELETE エンドポイントのテストでは、delete メソッドを使用して、特定の人物の URL (/persons/1) を渡します。今回は、JSON 本文も、返される expect も渡しません (削除済みのオブジェクトを DELETE エンドポイントから返すのは一般的な API 規約のように見えますが、実のところ、個人的にはあまり意味を感じたことがないので、返された JSON 本文はどのような目的でも使用したことがありません)。

5 つのテストが揃い、新しく拡張された npm script タグを package.json に含めました。残るは、npm test を実行するだけです。テスト ランナーがエラーなく機能し、ソース コードのコミットとプッシュを実行して、新しいコードで Azure 環境を更新するのを確認してください。

まとめ

mocha、supertest、should については、ここでは語りきれません。たとえば、ほとんどのテスト フレームワークと同様、mocha は各テスト周辺で実行される前後のフックをサポートします。また、supertest ライブラリは Cookie ベースの要求をサポートするので、ある種のセッション状態を維持するために Cookie を使用する HTTP エンドポイントを操作する際に便利です。さいわい、どのライブラリについても、非常にわかりやすい参考資料が提供されていて、Web では多数の使用例が公開されています。さて、どうやらスペースが尽きたようなので、いつものように締めくくります。コーディングを楽しんでください。


Ted Neward は、ポリテクノロジーに関するシアトルのコンサルティング サービス会社 Neward & Associates で CTO を務めています。これまでに 100 本を超える記事を執筆している Ted は、F# MVP で、さまざまな書籍を執筆および共同執筆しています。仕事への協力を依頼する場合、連絡先は ted@tedneward.com (英語のみ) です。また、blogs.tedneward.com (英語) でブログを公開しています。

この記事のレビューに協力してくれた技術スタッフの Shawn Wildermuth に心より感謝いたします。