みなさんはじめまして。

2011年11月に中途で入社し、現在はAmeba事業本部でスマートフォン版Ameba(通称デカグラフ)の開発をしている川口(facebook)と申します。

スマートフォン版Amebaではサーバー部分のNode.jsとフロント部分のJavaScriptを担当しています。趣味はWEBサービス制作で、最近の楽しみはぁゃぴ日報を読むことです。(社内ネタですすいません)

もともとは福岡のベンチャー企業で働いていたのですが、弊社が福岡に開発局を設けるということで福岡勤務を希望して応募したところ、なぜか東京の本社勤務となった次第です。

今回の記事では、JavaScriptのテスト手法ということで、フロントでのテストを前提として現在JavaScriptでよく使われているテストフレームワークと実際のテストコードの書き方を紹介していきます。

JavaScriptにおけるテストフレームワーク

JavaScriptにおけるテストフレームワークとしては、有名なもので現在以下のものがあります。

●JsUnit
Javaのテストフレームワークとして有名な「JUnit」を参考に作られたテストフレームワークです。

●QUnit
もともとjQueryをテストするために開発されたフレームワークですが、現在ではjQueryへの依存が無くなっているためjQuery以外のライブラリを使ったプロジェクトでも使用できます。

●Jasmine
RubyOnRailsでの開発でよく用いられる「RSpec」というフレームワークを参考に作られたテストフレームワークです。
ビヘイビア駆動開発(BDD)という概念に基づいて設計されており、テストコードそのものが要求仕様書のように見えるという特徴を持つフレームワークです。

●Mocha
Node.jsのフレームワークとして有名な「Express」と同じ作者が制作したテスト用のフレームワークです。
Jasmineと同じようにBDDの概念に基づいて設計されており、Expressとの親和性も高いため、Node.jsでのテストでは最有力のテストフレームワークの一つです。
フロントエンドでの使用も可能ですが、アサーション関数(テスト結果をチェックするための関数)が用意されていないため自前で用意する必要があります。

Jasmineを用いたテスト(準備編)

今現在の流れで言うと、全体としてはBDDの機能をもったテストフレームワークに人気が集まっていると感じています。

そのため、今回はJasmineを用いてフロントエンドのテストコードを書いていきます。

●使用するライブラリ
・jQuery(1.7.2)
・Jasmine(1.2.0)

●テストを実行するサンプル
JSのテスト手法を解説したブログなどを見ていつも思っていたことなのですが、細かい関数単位でのテストの書き方は解説していても、全体の動きを通したテストの書き方を解説しているところは少ないと感じていました。(そもそもそういうテストはSeleniumを使えと言われるかもしれませんが・・・)

そのため今回は全体の動きを通したテストとして、モーダルウィンドウを表示するサンプルをテスト対象として使用していきます。

HTML

<div id="root"></div>


JavaScript(modal.js)
(function(w) {


var exports = {};


var event = {

  documentReady: function() {

    $('#root').append(view.main);


    // ボタンのイベントをセット

    view.button.click(event.showModalWindow);

    view.main.append(view.button);

  },

  showModalWindow: function() {

    model.getDetail(function(detail) {

      view.modalWindow.find('.title').append(detail.title);

      view.modalWindow.find('.description').append(detail.description);

      view.main.append(view.modalWindow);

    });

  }

};



var model = {

  // 詳細情報の取得用関数

  getDetail: function(callback) {

    $.get('./detail.json', function(data) {

      callback(JSON.parse(data));

    });

  }

};



var view = {

  main: $('<div id="main"></div>'),

  button: $('<button>Show Modal Window</button>'),

  modalWindow: $('<div class="modal"><p class="title"></p><p class="description"></p></div>')

};


$(document).ready(event.documentReady);



// テスト用

exports.event = event;

exports.model = model;

exports.view = view;



return exports;

})(window);





サンプルの動きとしては、
1.ボタンをクリック
2.モーダルウィンドウが表示され、そこに商品などに関する詳細情報が表示される

というよくある流れです。

Jasmineを用いたテスト(実行編)

●テストを実行するための関数の用意
ここで、テストを実行するためのユーティリティ関数を用意していきます。
関数の内容としては以下の通りです。

・$.test.require
  テストの対象となるJSファイルを動的に読み込むための関数です。
  スクリプトタグを追加する形を取らず、$.getで取得したJSの文字列をevalで実行すること
  によって即時関数の返却値を取得し、テストを実行するためだけにグローバルな関数を
  定義することを減らすためのものです。

・$.test.exec
  Jasmineのテストを実行するための関数です。

Javascript(test.js)

(function(w) {


var slice = Array.prototype.slice;


$.test = $.test || {};




function isObject(o) {


  return o instanceof Object;


}




// テスト対象のJSを動的に読み込むための関数


$.test.require = function() {


  var args = slice.apply(arguments);


  var callback = args.pop();



  var len = args.length;



  


  var i = 0;



  var scripts = {};



  var exec = function() {



    var url = args[i];



    var beforeEval = null;



    var name = null;


    if (isObject(url)) {



      beforeEval = url.beforeEval;



      name = url.name;



      url = url.path;



    }



    $.get(url, function(script) {



      if (beforeEval) {



        script = beforeEval(script);



      }


      var resEval = eval(script);



      scripts[name || url] = resEval;



      i++;



      if (i === len && callback) {


        callback(scripts);


      } else {


        exec();


      }


    });


  };


  exec();


};







// テストの実行



$.test.exec = function() {



  var jasmineEnv = jasmine.getEnv();



  jasmineEnv.updateInterval = 1000;



  var htmlReporter = new jasmine.HtmlReporter();



  jasmineEnv.addReporter(htmlReporter);



  jasmineEnv.specFilter = function(spec) {



    return htmlReporter.specFilter(spec);



  };



  jasmineEnv.execute();



};



})(window);





●テストコード

全ての準備が整いましたので、ここでやっとテストコードを書いていきます!
テストの流れとしては、

1.ボタンをクリック
2.表示されたモーダルウィンドウの状態をチェック

という風になっています。

JavaScript(spec/modal.js)

(function() {





var modal = null;



var event = null;



var model = null;



var views = null;




$.test.require(



  // modal.jsの読み込み



  {name:'modal', path:'../js/modal.js', beforeEval:function(script) {



      // veiw.mainの#rootへのappendが実行されないようコードを削除



      return script.replace("$('#root').append(view.main);", '');



    }



  },



  function(module) {



    modal = module.modal;



    // modal.jsでexportsしておいたオブジェクトをここで取得できる



    event = modal.event;



    model = modal.model



    view = modal.view;



    // テストの実行



    $.test.exec();



  }



);







var base = null;



describe('モーダルウィンドウテスト', function() {



  var temp = null;



  var cnt = 0;





  beforeEach(function() {



    // 1度だけ実行されるようにする



    if (cnt > 0) { return; }



    



    // 詳細データ取得用の関数を一時的に書き換え



    temp = model.getDetail;



    model.getDetail = function(callback) {



      callback({title:'テスト・タイトル', description:'テスト・説明'});



    };



    // ボタンのクリック



    view.button.click();



    model.getDetail = temp;



    cnt++;



  });





  describe('表示チェック', function() {



    it('ウィンドウ', function() {




      expect(view.main.find('.modal').length === 1).toBe(true);



    });



    it('タイトル', function() {



      var title = view.modalWindow.find('.title').text()



      expect(title === 'テスト・タイトル').toBe(true);



    });



    it('説明', function() {



      var description = view.modalWindow.find('.description').text();




      expect(description === 'テスト・説明').toBe(true);



    });



  });});







})();





テストの結果はどうでしたでしょうか?
無事成功していれば以下のような画面が表示されると思います。


Jasmine成功画面


今回のサンプルをGithub上に上げていますので、よければそちらもご覧ください。

github|1pixelTestSample

おわりに

コードが多めの記事になってしまったため見づらかったかもしれませんが、最後までお付き合いいただきありがとうございました。
これを機にJavascriptでのテストに興味を持っていただければ幸いです。

実際にこのテスト手法を取り入れてスマートフォン版Amebaをリニューアルしていますので、よければご覧ください。

スマートフォン版Ameba


amebame.com