tanaka's Programming Memo

プログラミングについてのメモ。

Jestの公式サイトのTutorial

facebook.github.io

公式サイトのチュートリアルを試します。
JestによるJavaScriptの単体テスト - tanaka's Programming Memoで設定したフォルダーでそのまま作業を続けると良いでしょう。

下準備として、jQueryをインストールしておきます。

npm install --save jquery

公式サイトを意訳

非同期関数を利用するコードのテストをしてみましょう。コードの内容は以下の通りです。

  • Ajaxリクエストで現在のユーザーをJSON形式のデータで取得する
  • 取得したJSONデータを変換して、新しいオブジェクトを作成する
  • 作成した新しいオブジェクトをコールバック関数に渡して呼び出す

コードは以下の通りです。(公式サイトから転載。練習用のフォルダーにfetchCurrentUser.jsというファイルを作成して以下を入力)

// fetchCurrentUser.js
var $ = require('jquery');

function parseUserJson(userJson) {
  return {
    loggedIn: true,
    fullName: userJson.firstName + ' ' + userJson.lastName
  };
}

function fetchCurrentUser(callback) {
  return $.ajax({
    type: 'GET',
    url: 'http://example.com/currentUser',
    success: function(userJson) {
      callback(parseUserJson(userJson));
    }
  });
}

module.exports = fetchCurrentUser;


このモジュールのテストコードを書きます。テストコードは__tests__フォルダー内に、fetchCurrentUser-test.jsというファイルを新規に作成して、以下を入力します。

// __tests__/fetchCurrentUser-test.js
jest.dontMock('../fetchCurrentUser.js');

describe('fetchCurrentUser', function() {
  it('calls into $.ajax with the correct params', function() {
    var $ = require('jquery');
    var fetchCurrentUser = require('../fetchCurrentUser');

    // Call into the function we want to test
    function dummyCallback() {}
    fetchCurrentUser(dummyCallback);

    // Now make sure that $.ajax was properly called during the previous
    // 2 lines
    expect($.ajax).toBeCalledWith({
      type: 'GET',
      url: 'http://example.com/currentUser',
      success: jasmine.any(Function)
    });
  });
});

fetchCurrentUser.jsがあるフォルダー内で npm test を実行してテストします。

Jestは、ソースフォルダー内にある__tests__フォルダー内からテスト用のJavaScriptを自動的に見つけて実行していきます。

最初のコード(jest.dontMock('../fetchCurrentUser.js');)はとても重要です。これを省略した場合、require()したモジュールは全てモック(テスト用のハリボテ)になります。fetchCurrentUser.jsはテスト対象ですので、本物である必要があります。そこで「dontMock」で、モックにしないように設定しているのです。

このテストは、fetchCurrentUser()が指定のパラメータを渡して$.ajax()を呼び出すかを確認するものです。まずは、fetchCurrentUser()に仮のコールバック関数を渡して呼び出します。$.ajax()はモック関数なので、通信する代わりに渡された引数を記録します。その記録されたオブジェクトがtoBeCalledWith()の引数のオブジェクトの形式かをテストしています。

最初のテストはこれで完了です。しかし、まだテストとしては完全ではありません。$.ajaxの処理が成功した時のコールバックの動作がテストできていません。これをテストするためのコードが以下です(公式サイトから転載。コメントはこちらで追加。describe()のfunction(){}内に追加しましょう)。

  // $.ajaxが完了した時にコールバックを呼び出す
  it('calls the callback when $.ajax requests are finished', function() {
    var $ = require('jquery');  // これはモック
    var fetchCurrentUser = require('../fetchCurrentUser');  // これは本物

    // Create a mock function for our callback
    // --
    // コールバックのためにモック関数を作成
    var callback = jest.genMockFunction();
    // 作成したモックのコールバック関数を渡してテスト関数を呼び出す
    // $.ajax()はモックになっているので通信はされず、$.ajax()に指定した引数が記録される
    // 後で$.ajax.mock.callsという配列を参照して、記録された値を読み出してテストする
    fetchCurrentUser(callback);

    // Now we emulate the process by which `$.ajax` would execute its own
    // callback
    // --
    // 上で$.ajax()に設定したsuccess関数を、テスト用のパラメータを渡して実行
    // このテストコードによって、モック関数callbackの最初の呼び出しの最初の引数に、
    // parseUserJson()の結果が記録される
    $.ajax.mock.calls[0 /*first call*/][0 /*first argument*/].success({
      firstName: 'Bobby',
      lastName: '");DROP TABLE Users;--'
    });

    // And finally we assert that this emulated call by `$.ajax` incurred a
    // call back into the mock function we provided as a callback
    // --
    // 最後に、モック関数callbackの最初の実行の最初の引数に記録された値が、
    // 想定のものかをアサート(確認)して、テスト終了
    expect(callback.mock.calls[0/*first call*/][0/*first arg*/]).toEqual({
      loggedIn: true,
      fullName: 'Bobby ");DROP TABLE Users;--'
    });
  });

fetchCurrentUser()は、渡された引数を処理して、結果をcallbackに渡します。fetchCurrentUser()は、内部で$.ajax()を呼び出すので、依存関係があります。Jestはテストで発生する$.ajaxの全てのやり取りが可能なモックを作成します。それを実現するために、本物のモジュールを調査しているので、本物のモジュールも必要です。

Jestでは、全てのモック関数は「.mock」プロパティを持ちます。このプロパティはその関数の全てのやり取りの関数を保持します。今回の例では、中身をmock.callsから読み出しました。この配列は、その関数が何回目に呼ばれたかと、何番目の引数かを添え字にして情報を読み出すことができます。

npm test で、テストが成功することを確認しましょう。

以上で、非同期関数のテストが完了しました。ここで重要なのは、書いたコードが同期的に実行されることです。モック関数を利用する利点です。テスト対象のコードが同期であろうが非同期であろうが、テストコードは常に順次処理で実行できます。

このサンプルの完成コードは examples/tutorial/ にあります。