最先端のフルスタックアプリケーションには、バックエンドサービスが欠かせません。 フロントエンドサービスでバックエンドサービスを利用してデータを表示するアプリケーションなどは、その最たる例でしょう。 たいていの場合、このようなアプリケーションのテストにおいて、バックエンドサービスを丸ごと複製することは現実的ではありません。 このチュートリアルでは、API を実際に呼び出すのではなく API への HTTP リクエストを “モック” することで、エンドポイントをテストする方法について解説します。

Nock とは?

Nock は、HTTP サーバーを “モック” (実物の代わりを用意すること) して期待する応答を返すためのライブラリです。 このライブラリを使用すれば、HTTP リクエストを実行するフロントエンドモジュールのテストを簡易化できます。 また、API の応答を指定できるため、隔離環境でのテストも可能になります。

このチュートリアルでは、API を利用する既存の ToDo アプリケーションと Nock を使用して、このアプリケーションとバックエンド API とのやり取りをテストします。 ToDo アプリケーションはホスティング済みなので、アプリケーションの開発部分は気にせず、テストに注目してみてください。

前提条件

このチュートリアルで紹介する手順を余すところなく理解するには、以下の準備が必要です。

  1. JavaScript の基礎を理解する
  2. HTTP リクエストの基礎とテスト方法を理解する
  3. Node.js (バージョン 10 以上) をローカル システムにインストールする
  4. CircleCI アカウントを用意する
  5. GitHub アカウントを用意する

本チュートリアルはさまざまな CI/CD プラットフォームに適用できますが、例として CircleCI を使用しています。 CircleCI アカウントをお持ちでない場合は、こちらから無料アカウントを作成してください。

まず初めに、GitHub からテストリポジトリをクローンします。

リポジトリをクローンすれば、アプリケーションのセットアップは必要ありません。 後は、本チュートリアルの手順を進めながら、todo.spec.js ファイルで行われるステップを検証するだけです。 このファイルは、クローンしたリポジトリのアプリケーション ルートにあります。

モックテストを利用するメリット

シンプルなクライアント/サーバーアーキテクチャの Web アプリケーションでは、バックエンドサービスとフロントエンドサービスの両方を使用します。 フロントエンドサービスはアプリケーションのプレゼンテーションロジックを搭載し、一般にはユーザーインターフェースとして機能します。 ユーザーがアクションを実行すると、クライアントはバックエンドサービスにリクエストを送信します。 このチュートリアルでは、バックエンドサービスはサーバーに相当します。

モックテストでは、サーバーや外部システムを使用する必要がなくなるため、テスト実行プロセスの時間を短縮できます。 “モック” とは、いわばバックエンドサービスの実物大模型を用意するようなものです。 依存関係をモックすると、時間短縮以外にも、データベースなどの外部システムリソースに負荷をかけずにテストを実施できるというメリットもあります。

注: テストカバレッジが過不足のないものになるように、モックテストは常に他の種類のテストと併用することをお勧めします。 最適なテストを行えば、リリースプロセスの信頼性を高められます。

API HTTP アーキテクチャをテストする

1 つ目のサンプルテストでは、todo アプリケーションの実際のバックエンドにリクエストを送信した場合にかかる時間を計ります。 このリクエストを見ておくことで、モックテストを使用した場合、特にバックエンドをモックするテストを実行した場合に、時間をどれだけ節約できるかを比較できます。

ユーザー目線では、このアプリケーションは todo 項目を作成、表示、取得します。 テストでは、作成済みの todo 項目すべてを取得するのに要する時間を確認します。

describe('todo-app-barkend mocked tests', () => {
   it('can get todos', async () => {
       const response = await getTodoItems.getAllTodos();
       console.log(response)
   })
})

このシンプルなテストを実行すると、結果が返されるまでの時間がわかります。 このテストでは実際のデータベースを呼び出しているため、必要な結果だけでなく、すべての結果が返ってきます。

アプリケーションランタイム

追加済み項目すべてを取得するのに要した時間は、約 3.591 秒でした。 つまり、テストの実行中に応答内の要素についてアサーションを行おうとすると、そのたびに項目の取得に約 3.591 秒かかることになります。 そのため、テストの実行にかなりのオーバーヘッドと時間的制約が加わります。

テスト実行アーキテクチャ: モックを使用しない場合

下図は、サービスをモックしない場合のテスト実行の仕組みを簡単に示したアーキテクチャ図です。

非モックAPIテストアーキテクチャ

このアーキテクチャをテストするにあたり、テスト用のデータベースをセットアップして応答を正しく得られるようにするのは、かなり煩雑な作業です。 代わりに、バックエンドの依存関係をすべて切り離し、フロントエンドアプリケーションだけに集中できる方法があります。 それが、Nock を使用したモックです。Nock は、バックエンド API へのリクエストをインターセプトし、事前に設定された応答を返すモックサービスです。

テスト実行アーキテクチャ: モックを使用した場合

Mocked API テストアーキテクチャ

このアーキテクチャ図から、クライアントに返ってくるのが実際の API からの応答ではなく、あらかじめ設定しておいた応答であることがわかります。

必要であれば、モックの API 応答をオーバーライドして、エンドポイントに直接リクエストを投げることもできます。 以下のセクションで、Nock ライブラリを使用して、テスト用に API サービスをモックする方法と、モックした呼び出しをオーバーライドする方法を見ていきましょう。

その前に、リポジトリに CircleCI と Git をセットアップしてください。 既にリポジトリをクローンし、アプリケーションをセットアップ済みである場合は、このステップは必要ありません。

Git をセットアップして CircleCI にプッシュする

CircleCI をセットアップするには、以下のコマンドを実行してプロジェクトの Git リポジトリを初期化します。

git init

次に、ルートディレクトリに .gitignore ファイルを作成します。 このファイル内に node_modules を追加して、npm で生成されたモジュールがリモート リポジトリに追加されないように設定します。 次の手順として、コミットを追加し、プロジェクトを GitHub にプッシュします。

CircleCI にログインして、[Projects (プロジェクト)] に移動します。 お使いの GitHub ユーザー名または組織に関連付けられているすべての GitHub リポジトリが一覧表示されます。 本チュートリアル用に CircleCI にセットアップするリポジトリは、api-mock-testing-with-nock です。

[Projects (プロジェクト)] ダッシュボードで、[Set Up Project (プロジェクトのセットアップ)] ボタンをクリックします。 次に、[Use Existing Config (既存の設定ファイルを使用する)] をクリックします。

ページ構築開始

プロンプトが表示されたら、[Start Building (ビルドの開始)] をクリックします。 パイプラインが失敗しますが、これは想定どおりです。 プロジェクトを正常にビルドするには、.circleci/config.yml 設定ファイルをカスタマイズして GitHub に追加する必要があるからです。

コンフィグページを選択

CI パイプライン設定ファイルを作成する

CircleCI パイプラインのセットアップが完了したら、続いては CircleCI をローカルプロジェクトに追加します。 まず、ルートディレクトリに .circleci というディレクトリを作成します。 このディレクトリの中に config.yml ファイルを作成します。 このファイルに以下の設定を追加します。

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:10.16.3
    steps:
      - checkout
      - run:
          name: update npm
          command: "npm install -g npm@5"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: run api mock tests
          command: npm test
      - store_artifacts:
          path: ~/repo/api-mock-testing-with-nock

この設定ファイルでは、環境から Node の Docker イメージをプルし、npm パッケージマネージャーを更新しています。 キャッシュが存在する場合は、キャッシュをリストアします。そして、save_cache の実行時点で変更が検出された場合は、アプリケーションの依存関係を更新します。 最後に、api-mock-testing-with-nock テストを実行し、キャッシュ済みの項目を api-mock-testing-with-nock ディレクトリ内のアーティファクトに保存します。

変更を GitHub にプッシュすると、CircleCI によって自動的にビルド プロセスが開始されます。 まだテストを作成していないため、パイプラインは再び失敗します。 テストを追加した後で再度実行するので、現時点ではこれで問題ありません。 テストがなくても、パイプラインの詳細を確認できます。

Nock を使用してテストする

アーキテクチャ図で見たように、Nock はバックエンドアプリケーションとフロントエンドアプリケーションの間に配置され、テスト対象のすべてのリクエストをインターセプトします。 これにより、実際にバックエンドサービスを呼び出してアプリケーションをテストする代わりに、既知の応答一式を事前に設定することでバックエンドサービスをシミュレート (モック) できます。

以下のテストでは、先ほどのテストを書き換えて、todo 項目の応答を返す API リクエストをモックします。 テストは、クローンしたリポジトリ内のファイル todo.spec.js にあります。

it('can get todos', async () => {
       const todoObject = {
           todos: [
               { task: "Two", _id: 9, "completed": false },
               { task: "three", _id: 84, "completed": false }]
       }
       nock('https://todo-app-barkend.herokuapp.com/todos/')
           .get('/')
           .reply(200,
               todoObject
           )
       const res = await got('https://todo-app-barkend.herokuapp.com/todos/')
       expect(res.body).to.eq(JSON.stringify(todoObject))
       expect(res.statusCode).to.equal(200)
   })

このテストでは、Nock が /todos/ エンドポイントにルーティングされるすべての GET リクエストをインターセプトします。 そして、API からの実際の応答ではなく、nock() メソッドで定義された応答を返します。

注: Heroku の無料版 Dyno は、30 分間操作しないとスリープします。 テストに失敗する場合、Dyno がアクティブでないことが原因の可能性があるため、もう一度テストを実行してみてください。

テストの実行時間を見てみると、実際のサービスを使ったテストよりモックを使ったテストのほうが速いことがわかります。 サービスをモックした場合の実行時間は約 1.58 秒でした。 先ほどのサービスをモックしないリクエストの実行時間が約 3.59 秒だったのと比べると、かなり良い結果と言えるでしょう。

モックランタイムによる実行

テストの結果を見ると、リクエストをモックすることで実行時間を短縮し、開発時間を大幅に節約できることがわかります。 このメリットは、バックエンドを呼び出す必要のあるテストを複数実行する場合に特に顕著です。

注: Nock は API リクエストをモックしますが、エンドポイントが存在することを確認するためにテスト中にエンドポイントに HTTP リクエストを送信する必要があります。 そのため、モック対象のエンドポイントが使用できない場合や、テストで指定したルートが有効でない場合、テストが失敗します。

Nock の HTTP モックをバイパスする

エンドポイントのモックは、アプリケーションとバックエンドサービスとのやり取りを理解するのに役立ちますが、テストではリクエストのモックをバイパスしたい場合もあります。 たとえば、バックエンドサービスに HTTP リクエストを送信したときに、実際のエンドポイントから適切な応答が得られるかどうかを確認したい場合などです。

次のサンプルテストでは、Nock の enableNetConnect メソッドを使用して、テストのモックをバイパスするリンクを定義しています。 このメソッドの上手な使用例として、localhost URL への接続は有効にしつつ、ホスティング URL にはモックを使用すれば、すべてが正しく動作していることを確認できます。 今回のテストは、クローンしたリポジトリ内のファイル todo.spec.js にあります。

it('can create todos - enabled backend', async () => {
       var options = {
           headers: {
               'content-type': 'application/json'
           },
           body: JSON.stringify({
             task: 'Cook Lunch'
           })
         };

       nock.enableNetConnect(/(todo-app-barkend)\.herokuapp.com/)
       const res = await got.post('https://todo-app-barkend.herokuapp.com/todos/', options)
       expect(JSON.parse(res.body)).to.have.property('task', "Cook Lunch");
   })

todo 項目を作成するためのバックエンドのテストで、バイパスする URL の正規表現を指定しています。 そのうえで、その URL に対してリクエストを送信しています。 次のリクエストで、リモート URL へのリクエスト送信時にアプリケーションが実際に todo 項目を作成していることを確認しています。

モックとブロックをクリアする

テストを実行した後は、忘れずにモックをクリアする必要があります。 また、HTTP リクエストをブロックした場合には有効状態に戻すことも重要です。 モックやブロックをクリアすることで、後続のテストに影響を与えず、HTTP リクエストを正常に行えるようになります。

テストの実行後にモックをクリアするには、テストの afterEach セクションに次のコードを記述します。

afterEach(() => {
       nock.cleanAll()
       nock.enableNetConnect()
   })

CircleCI パイプラインが成功することを確認する

テストが完成し、CI パイプラインのセットアップも完了したので、すべてのファイルを Git に追加して GitHub のリモートリポジトリにプッシュしましょう。 CircleCI パイプラインが開始され、自動的にテストが実行されるはずです。 パイプラインの実行を観察するには、CircleCI ダッシュボードに移動し、プロジェクト名 (api-mock-testing-with-nock) をクリックします。

ビルドのステータスを確認するには、CircleCI ダッシュボードで目的のビルドを選択します。 CircleCI 設定ファイルに定義された各ステップのステータスを確認することができます。

パイプラインの実行に成功

パイプラインの実行を観察するには、CircleCI ダッシュボードに移動し、プロジェクト名 (api-mock-testing-with-nock) をクリックします。

ビルドのステータスを確認するには、CircleCI ダッシュボードで目的のビルドを選択します。 CircleCI 設定ファイルに定義された各ステップのステータスを確認することができます。

ビルド確認

うまくいきましたね。 ビルドが無事に成功しました。

おわりに

このチュートリアルでは、API モックの内容とメリットについてご紹介しました。 また、テストで Nock を使用して HTTP リクエストをモックする方法を解説し、モックを使うことでテストの実行時間を短縮できることも確認しました。 さらに、外部の依存関係を介さずに、隔離環境でアプリケーションの動作だけをテストする方法も説明しました。

モックを使用してテストを行う方法を練習し、モックをクリアすることの重要性とタイミングも確認しました。 これらの知識があれば、実際に API を呼び出して応答を得ることなく、API のテストを効率的に行えるようになります。 すばらしい (高速な) テストができますように!


Waweru Mwaura 氏は品質工学を専門とするソフトウェア エンジニアです。生涯学習の実践者という顔も持ち、 Packt で執筆者を務めながら、工学や金融、テクノロジーの書籍を愛読しています。 Mwaura 氏の詳細な情報は、こちらの Web プロフィールをご覧ください。

さんの他の投稿を読む Waweru Mwaura