JS開発におけるTDDと自動テストツール利用の勘所 | PPT | Free Download
SlideShare a Scribd company logo
JS開発における
              TDDと自動テスト
              ツール利用の勘所
                   2012.12.06
               株式会社マピオン 中村 浩士



12年12月5日水曜日
自己紹介
              中村 浩士 ( @kozy4324 )

              株式会社マピオン所属

              主にWebアプリのフロントエンド開発

              JavaScript, ActionScript




12年12月5日水曜日
Mapion




12年12月5日水曜日
Mapion
              地図情報検索サイト
              月間7600万PV、1200万UU
              全文検索エンジンSolrを利用した900万件超
              のスポット情報を検索できる電話帳/地図面
              その他位置情報コンテンツやナビサービス
              2008年からスマートフォンサイトにも注力
              Android、iOSのネイティブアプリ開発も


12年12月5日水曜日
地図を使ったWebアプリ
           よく開発しています




12年12月5日水曜日
今回 話すること
              TDDの基本となるJSユニットテストツールの
              使い方

              WebアプリでのTDDを意識した設計について
              (少しだけ)

              様々なツールを利用してTDD/自動テストの効
              率化を試みる話




12年12月5日水曜日
話しないこと
              TDD自体について
               詳細なやり方、あるべき論
               WebアプリでのTDDベストプラクティス
               僕はまだその答えに辿り着いていないです...
               「テスト駆動JavaScript」が良書なので、それを読みましょう



              Webアプリに対するシナリオベースの    
              自動テストについて
               ユーザー操作をエミュレートしてWebアプ
               リ全体の振る舞いを自動でテストする方法


12年12月5日水曜日
アジェンダ
              ブラウザ上で実行するJSユニットテストツール
               各ツール比較
               使い方&コードサンプル
              WebアプリのTDDを意識した設計について
              TDDや自動テストで活用できる各種ツール
               コマンドライン環境
               ヘッドレスブラウザ
               自動テストツール
               CI環境


12年12月5日水曜日
ブラウザ上で実行する
              JSユニットテストツール



12年12月5日水曜日
TDDとは
              Test-Driven Development(テスト駆動開発)
              分析技法、設計技法( テスト技法)
              正しく動くソフトウェアを確実に作り上げるため
              のテクニック
              進め方
              1. テストを書く(テストファースト)
              2. テストをパスする最低限の実装を行う
              3. テストのパスを保持したままコードの重複を除
                 去する(リファクタリング)
              4. 1∼3を短いスパンで繰り返す

12年12月5日水曜日
TDDの効果
              書いたプログラムに対する即座のフィードバック
              要求の理解の促進
              リファクタリングの支援、クリーンコードの促進
              自動テストによるデグレード検知
              プログラマが持つ不安の解消
               心の健康をもたらす :)




12年12月5日水曜日
JSユニットテストツール
              JsUnit
              YUI Test
              Google Closure Tools
              QUnit
              Jasmine
              Mocha
              Vows
              (etc...)


12年12月5日水曜日
JSユニットテストツール
              JsUnit
              YUI Test
              Google Closure Tools
              QUnit       自分がよく利用するのはこの4つ
                          QUnit, Jasmine, Mochaはブラウザ上で実行可能
              Jasmine
              Mocha
              Vows
              (etc...)


12年12月5日水曜日
ざっくり比較
                                                       非同期
                    スタイル              ブラウザ実行   CLI実行
                                                       サポート


                                                                  シンプル
        QUnit         フラット              ○       △       ○      ブラウザ実行に最適




                                                                   Rubyist向け
       Jasmine         BDD              ○       ○       ○      Jasmine-gemくそ便利




                 BDD, TDD, Exports,                              Nodeモジュール
        Mocha     フラットが選べる              ○       ○       ○         フレキシブル



                                                                  Nodeモジュール
        Vows          Exports                   ○       ○     Nodeの非同期処理テストが
                                                                  スマートに書ける




12年12月5日水曜日
どれを利用すればよい?




12年12月5日水曜日
ケース別
              プロジェクトへの導入が目的
                 シンプルなQUnitがオススメ

              Ruby / Ruby on Railsがメインの領域な人
                 Jasmineがオススメ

              CLI得意 / Node.jsもやりたい!
                 Mocha, Vowsがいいのでは?


12年12月5日水曜日
QUnitとJasmineの
                使い方を紹介



12年12月5日水曜日
QUnit




12年12月5日水曜日
QUnitとは?
              ブラウザ上での実行を想定したJSユニットテ
              ストフレームワーク

              jQueryの開発に利用されている

              シンプルさが特徴

              MITライセンス

              現在のリリースバージョンは v1.10.0


12年12月5日水曜日
http://qunitjs.com




                        サイトからjsとcssをダウンロードして利用可能




12年12月5日水曜日
npmインストールの場合
              パッケージ指定してインストール
              $ npm install qunitjs
              $ ls node_modules/qunitjs/qunit/
              qunit.css!qunit.js



              もしくはpackage.json記述してインストール
              $ cat package.json
              {
                "name": "sample-of-tdd",
                "version": "1.0.0",
                "devDependencies": {
                  "qunitjs": "1.10.0"
                }
              }
              $ npm install




12年12月5日水曜日
HTML記述例
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>objects</title>                     qunit.js, qunit.css, テスト対象のjsファイル, テスト
    <link rel="stylesheet" href="qunit.css">   コードを記述したjsファイルの4リソースを読み込む
    <script src="qunit.js"></script>
                                               titleを設定することを強く推奨
    <script src="objects.js"></script>
    <script src="objects_test.js"></script>
    </head>
    <body>
    <div id="qunit"></div>                     id="qunit"の要素に結果が出力される
    <div id="qunit-fixture"></div>             id="qunit-fixture"はテスト実行の度に初期状態に復元
    </doby>
                                               されるのでDOMに依存したテストの場合に利用できる
    </html>




12年12月5日水曜日
テストの基本構造
     module("Object");
     test("#methodA", function(assert) {
                                           module → test → アサーションの3階層
       assert.ok(true, "some messages");
     });
     test("#methodB", function(assert) {
       assert.ok(true, "some messages");
       assert.ok(true, "some messages");
     });

     module("Array");
                                           次のmodule関数を呼ぶまでのtest関数がグルーピングされる
     test("#methodA", function(assert) {
       assert.ok(true, "some messages");   (関数をネストする形ではない → フラットな形式)
       assert.ok(true, "some messages");
     });
     test("#methodB", function(assert) {
       assert.ok(true, "some messages");
       assert.ok(true, "some messages");
       assert.ok(true, "some messages");
     });




12年12月5日水曜日
アサーション
              ok(state[, message])
              equal(actual, expected[, message])
              notEqual(actual, expected[, message])
              deepEqual(actual, expected[, message])
              notDeepEqual(actual, expected[, message])
              strictEqual(actual, expected[, message])
              notStrictEqual(actual, expected[, message])
              throws(block, expected[, message])
                                     CommonJS Unit Testingの仕様に追従している


12年12月5日水曜日
setup/teardown
     module("Object", {
       setup: function() {
         this.object = new MyObject();     module()の第2引数のオブジェクトにsetupとteardownを設定
       },
       teardown: function() {              で、テスト実行毎の前処理/後処理が行える
         // do something...
       }
     });
     test("#methodA", function(assert) {
       assert.ok(this.object.methodA());   thisでスコープが共有(ただしテスト毎のthisは別オブジェクト)
     });
     test("#methodB", function(assert) {
       assert.ok(this.object.methodB());
     });

     module("Array", {
       setup: function() {
         this.array = new MyArray();       setup/teardownはmodule単位で別に設定できる
       }
     });
     test("#methodA", function(assert) {
       assert.ok(this.array.methodA());
     });
     test("#methodB", function(assert) {
       assert.ok(this.array.methodB());
     });




12年12月5日水曜日
expect()
     test("#forEach with 1 item", 1, function(assert) {
       [1].forEach(function(){
                                                          テスト内のアサーション数をチェック
         assert.ok(true);
       });                                                コールバック振る舞いの確認に利用可能
     });
                                                          test()の引数に指定もしくは
     test("#forEach with 2 items", function(assert) {
                                                          expect()関数で指定する
       expect(2);
       [1,2].forEach(function(){
         assert.ok(true);
       });
     });




                     ただし、expect()だけでのコールバック振る舞いテストは貧弱なので
                     複雑なケースはSinon.jsを利用したほうがよい




12年12月5日水曜日
非同期処理のテスト
     test("asyncTest A", function(assert) {
       expect(1);                                  stop()で次テストの実行を保留
       setTimeout(function() {
                                                   start()で保留を解除する
         assert.ok(true);
         start();
       }, 1000);
       stop();
     });

     asyncTest("asyncTest B", function(assert) {   test() → asyncTest()とすることで
       expect(1);
                                                   stop()を省略できる
       setTimeout(function() {
         assert.ok(true);
         start();
       }, 1000);
     });




12年12月5日水曜日
実行結果




12年12月5日水曜日
実行結果
              onでグローバルへの変数汚染を
              チェックするモードで再実行                       モジュールでの絞り込み実行も可能




                         リストをクリックすると詳細を開閉
                         (エラー時は最初から開いている)

                         Rerun選択 or ダブルクリックで
                         特定テストのみ再実行




                            再実行時はfailしたテストから実行する(sessionStorage利用してる)
                            その仕様を知らずに順番に依存したテストを書くと死ねます...


12年12月5日水曜日
failした時はこんな感じ




12年12月5日水曜日
Jasmine




12年12月5日水曜日
Jasmineとは?
              RubyのRSpecライクな記法のBDD(ビヘイ
              ビア駆動開発)フレームワーク
              豊富なExpectationsとMatchers
              (QUnitで言うアサーション)
              spyによるTest Double(テスト代役)
              プラガブルなReporter
              MITライセンス
              現在のリリースバージョンは v1.3.0


12年12月5日水曜日
https://github.com/pivotal/jasmine




                         GitHubプロジェクトページのDownloadsにある
                         zipファイルをダウンロードして解凍



12年12月5日水曜日
補足:関連プロダクト

                                   GitHub: pivotal/jasmine-gem
                                   RubyGems: jasmine
                                   npm: -
                              依存
                                   RackやSeleniumを含めた実行ヘルパー
    GitHub: pivotal/jasmine
    RubyGems: jasmine-core
    npm: -
    JavaScriptのフレームワーク部分
                                   GitHub: mhevery/jasmine-node
   ダウンロードした                   依存   RubyGems: -
   standalone版はコレ
                                   npm: jasmine-node
                                   Nodeで実行するためのCLIラッパー

                                        jasmine-gemやjasmine-nodeについては後半で


12年12月5日水曜日
zipファイルの中身
              $ tree
              .
              !"" SpecRunner.html            htmlがすでにサンプルとして
              !"" lib                        動くものになっている
              #   $"" jasmine-1.3.0
              #        !"" MIT.LICENSE
              #        !"" jasmine-html.js
              #        !"" jasmine.css
              #        $"" jasmine.js
              !"" spec
              #   !"" PlayerSpec.js
              #   $"" SpecHelper.js
              $"" src
                  !"" Player.js
                  $"" Song.js

              4 directories, 9 files




12年12月5日水曜日
SpecRunner.htmlの中身
     <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
       "http://www.w3.org/TR/html4/loose.dtd">
     <html>
     <head>
       <title>Jasmine Spec Runner</title>

       <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.0/jasmine_favicon.png">
       <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.0/jasmine.css">
       <script type="text/javascript" src="lib/jasmine-1.3.0/jasmine.js"></script>
       <script type="text/javascript" src="lib/jasmine-1.3.0/jasmine-html.js"></script>

       <!-- include source files here... -->
       <script type="text/javascript" src="src/Player.js"></script>
       <script type="text/javascript" src="src/Song.js"></script>

       <!-- include spec files here... -->
       <script type="text/javascript" src="spec/SpecHelper.js"></script>
       <script type="text/javascript" src="spec/PlayerSpec.js"></script>

       <script type="text/javascript">
         (function() {
           var jasmineEnv = jasmine.getEnv();
           jasmineEnv.updateInterval = 1000;                  テスト対象のコードとスペックファイル(テストコード)を
           :                                                  それぞれ追加していけばよい
           (がっつり初期化処理が書いてあるので省略)
           :

         })();
       </script>

     </head>

     <body>
     </body>
     </html>




12年12月5日水曜日
テストの基本構造
              describe("Array", function() {
                describe(".isArray", function() {
                  it("should return true when called with an array", function() {
                    expect(Array.isArray([])).toBeTruthy();
                  });
                });

                describe("(has no item)", function() {
                  describe("#join", function() {
                    it("should return an empty string", function() {
                      expect([].join()).toEqual("");
                    });
                  });
                });
              });
                                                    describe → it → expectationsの3階層
                                                    describeはネストして記述することが可能
                                                    ex) describe → describe → it → expectations




12年12月5日水曜日
Matchers
              expect(x).toEqual(y)
              expect(x).not.toEqual(y)
              expect(x).toBe(y)                  notで否定のMatcherとなる
              expect(x).toMatch(pattern)
              expect(x).toBeDefined()             toBeは === による等値チェック
              expect(x).toBeUndefined()
              expect(x).toBeNull()
              expect(x).toBeNaN()
              expect(x).toBeTruthy()
              expect(x).toBeFalsy()
              expect(x).toContain(y)
              expect(x).toBeLessThan(y)
              expect(x).toBeGreaterThan(y)
              expect(x).toBeCloseTo(y, precision)
              expect(function(){fn();}).toThrow(e)
              expect(spy).toHaveBeenCalled()
              expect(spy).toHaveBeenCalledWith(arguments)
              and more ...
                                              詳しくはGitHubのwikiページを参照


12年12月5日水曜日
beforeEach/afterEach
    describe("Object", function() {
       var object;
      beforeEach(function() {
        object = new MyObject();            describeのスコープ内でbeforeEach/afterEachを設定すること
      });
      afterEach(function() {                で、 テスト実行毎の前処理/後処理が行える
        // do something...
      });
       describe("#methodA", function() {
       it("should be ok", function() {
           expect(object.methodA()).toBeTruthy();
         });
       });
       describe("#methodB", function() {
       it("should be ok", function() {
           expect(object.methodB()).toBeTruthy();
         });
       });
       describe("(context)", function() {
         beforeEach(function() {
           object.someMethod();
         });
         describe("#methodC", function() {
           it("should be ok", function() {
             expect(object.methodC()).toBeTruthy();
           });
         });
       });
                              ネストしたdescribeそれぞれで設定した場合、親子関係の順でコールバックされる
    });                       親のbeforeEach → 子のbeforeEach → 子のafterEach → 親のafterEach

12年12月5日水曜日
spy
     it("should be called", function() {          spyOnメソッドでオブジェクトの
       var obj = {method: function() {}};         特定メソッドをスパイ化
       spyOn(obj, "method");
       obj.method();
                                                  spy用のMatcherが用意されている
       expect(obj.method).toHaveBeenCalled();
     });                                          詳しくはGitHubのwikiページを参照

     test("should be called", function() {
                                                  jasmine.createSpy()関数でスパイ化
       var spy = jasmine.createSpy();
       spy();                                     された関数オブジェクトを作成
       expect(spy).toHaveBeenCalled();
     });



                            Jasmineのspyオブジェクトは強力で十分な機能を
                            有しているが、Sinon.jsのほうが高機能




12年12月5日水曜日
非同期処理のテスト
     it("should be async", function() {
       runs(function() {                  非同期処理ブロックはruns()で定義される
         expect(true).toBeTruthy();
       });
                                          waits()で次のブロックの実行を、指定した
       waits(500);
                                          ミリ秒間保留する
       var spy = jasmine.createSpy();
       runs(function() {
         setTimeout(spy, 1000);
       });

       waitsFor(function() {
                                          waitsFor()はコールバックがtrueを返す
         return spy.callCount > 0;
       });                                まで、次のブロック実行を保留する

       runs(function() {
         expect(true).toBeTruthy();
       });
     });




12年12月5日水曜日
実行結果




12年12月5日水曜日
実行結果
      spec毎のpass or failの結果




                                specのテキストをクリックすると、
                                該当スペックのみを再実行




12年12月5日水曜日
failした時はこんな感じ




12年12月5日水曜日
QUnit vs Jasmine




12年12月5日水曜日
module("Array");
              test(".isArray", function(assert) {
                assert.ok(Array.isArray([]), "Arrayでtrue");
              });

              module("Array.prototype", {
                setup: function() {
                  this.array = [1,2,3];
                  this.empty_array = [];
                }
              });
              test("#concat", function(assert) {
                assert.deepEqual(this.array.concat(), [1,2,3], "引数なしは配列のコピーを返す");
                assert.deepEqual(this.array.concat(4), [1,2,3,4], "引数を末尾に連結した配列を返す");
                assert.deepEqual(this.array.concat(4,5), [1,2,3,4,5], "引数は可変長に指定できる");
                assert.deepEqual(this.array.concat([4,5]), [1,2,3,4,5], "配列は展開されて連結される");
              });
              test("#join", function(assert) {
                assert.equal(this.array.join(), "1,2,3", "カンマで連結された文字列を返す");
                assert.equal(this.array.join("-"), "1-2-3", "引数の文字列で連結された文字列を返す");
                assert.equal(this.empty_array.join(), "", "要素がない配列は空文字列を返す");
                assert.equal(this.empty_array.join("-"), "", "セパレーターを指定しても空文字列");
              });
              test("#pop", function(assert) {
                assert.equal(this.array.pop(), 3, "末尾の要素を返す");
                assert.deepEqual(this.array, [1,2], "戻り値の要素が削除される");
                assert.equal(this.empty_array.pop(), undefined, "空配列はundefinedを返す");
              });
              test("#push", function(assert) {
                assert.equal(this.empty_array.push(1), 4, "引数の要素を追加した後のサイズを返す");
                assert.deepEqual(this.empty_array, [1,2,3,1], "要素が配列に追加される");
                assert.equal(this.empty_array.push(2,3), 6, "引数の要素を追加した後のサイズを返す");
                assert.deepEqual(this.empty_array, [1,2,3,1,2,3], "全ての要素が配列に追加される");
              });


12年12月5日水曜日
describe("Array", function() {
                describe(".isArray", function() {
                  it("should return true when called with an array", function() {
                    expect(Array.isArray([])).toBeTruthy();
                  });
                });

                describe("(has 3 items)", function() {
                  var array;
                  beforeEach(function() {
                    array = [1,2,3];
                  });
                  describe("#concat", function() {
                    it("should return an array of own copy when called with no argument", function() {
                      expect(array.concat()).toEqual([1,2,3]);
                    });
                    it("should return an array including passed argument", function() {
                      expect(array.concat(4)).toEqual([1,2,3,4]);
                      expect(array.concat(4,5)).toEqual([1,2,3,4,5]);
                    });
                    it("should return an array including passed argument with array splatting", function() {
                      expect(array.concat([4,5])).toEqual([1,2,3,4,5]);
                    });
                  });
                  describe("#join", function() {
                    it("should return a string joined items with comma when called with no argument", function() {
                      expect(array.join()).toBe("1,2,3");
                    });
                    it("should return a string joined items with passed argument", function() {
                      expect(array.join("-")).toBe("1-2-3");
                    });
                  });
                  describe("#pop", function() {
                    it("should return and remove the last item", function() {
                      expect(array.pop()).toBe(3);
                      expect(array).toEqual([1,2]);
                    });
                  });
                  describe("#push", function() {
                    it("should add arguments into own, and return own size", function() {
                      expect(array.push(1)).toBe(4);
                      expect(array).toEqual([1,2,3,1]);
                      expect(array.push(2,3)).toBe(6);
                      expect(array).toEqual([1,2,3,1,2,3]);
                    });
                  });
                });

                describe("(has no item)", function() {
                  var array;
                  beforeEach(function() {
                    array = [];
                  });
                  describe("#join", function() {
                    it("should return an empty string", function() {
                      expect(array.join()).toBe("");
                      expect(array.join("-")).toBe("");
                    });
                  });
                  describe("#pop", function() {
                    it("should return undefined", function() {
                      expect(array.pop()).toBeUndefined();
                    });
                  });
                });
              });



12年12月5日水曜日
QUnit vs Jasmine
              ブラウザ上の実行では基本機能は同等

              記述スタイルの違い、好みの問題

               QUnitはボキャブラリーが絞られるので 
               簡潔にならざるを得ない、表現力は劣る

               Jasmineは構造化しやすいがネストが深く
               なりがち(平均3∼5)、記述量も多め



12年12月5日水曜日
WebアプリのTDDを
              意識した設計について



12年12月5日水曜日
TDDやりづらい実装
              host objectに強依存
               host objectとは実行環境から提供されるオ
               ブジェクト
               ex) window, navigator, location, etc...

              DOMオブジェクトに強依存




12年12月5日水曜日
host objectに強依存
              location.searchのクエリーストリングをオブ
              ジェクトに変換する関数の実装
              function parseQuery() {
                var obj = {}, kvs = location.search.substring(1).split("&");
                kvs.forEach(function(kv){obj[kv.split("=")[0]]=kv.split("=")[1]});
                return obj;
              }
              query = parseQuery();


              locationオブジェクトへの参照を外に出すだ
              けでユニットテストは書きやすくなる
              function parseQuery(search) {
                var obj = {}, kvs = search.substring(1).split("&");
                kvs.forEach(function(kv){obj[kv.split("=")[0]]=kv.split("=")[1]});
                return obj;
              }
              query = parseQuery(location.search);



12年12月5日水曜日
host objectに強依存
              どうしても引数を指定しないI/Fを作成したい
              のであれば、ラッパー関数で分離
              function parseQuery() {
                return _parseQuery(location.search);
              }
              function _parseQuery(search) {
                var obj = {}, kvs = search.substring(1).split("&");
                kvs.forEach(function(kv){obj[kv.split("=")[0]]=kv.split("=")[1]});
                return obj;
              }
              query = parseQuery();




12年12月5日水曜日
DOMオブジェクトに強依存

              例えばjQueryでありがちがコード
              $(function(){
                $("div li .button").click(function(){
                  $("div .contents").html("<span>"+$(this).data("mydata")+"</span>");
                })
              })

              DOMに依存することで発生する問題点
                 DOM要素が存在しないと実行できない
                 DOM操作に対する副作用の検証(アサー
                 ション)が大抵のケースで非常に難しい
                 UIに伴って変更されやすいHTML構造に依
                 存してしまう(上記ではセレクター部分)

12年12月5日水曜日
DOMオブジェクトに強依存

              問題に対するアプローチ
               DOMに依存しない部分を分離する
               $(function(){
                 $("#button").click(function(){
                   clickHandler($("#contents"), $(this).data("mydata"));
                 })
               })
               function clickHandler(elm, data) {
                 elm.html("<span>"+data+"</span>");
               }



               DOM操作の振る舞いのみをテストする
               it("should call html() of passed element", function() {
                 var fakeObj = {html: jasmine.createSpy()};
                 clickHandler(fakeObj, "hoge");
                 expect(fakeObj.html).toHaveBeenCalledWith("<span>hoge</span>");
               });



               可能であればHTML構造に依存しない  
               セレクタ(idセレクタなど)に変更
12年12月5日水曜日
host object/DOMへの
              依存を分離した設計で
              Nodeなどの別環境でも
              ユニットテストが書ける

12年12月5日水曜日
TDDや自動テストで
              活用できる各種ツール



12年12月5日水曜日
コマンドライン環境
                 (CLI)



12年12月5日水曜日
CLIでTDDする動機
              ブラウザ実行での コード修正→保存→アプリ
              ケーション切替→ブラウザ再読み込み、この
              手順が煩雑

              ブラウザ実行ではテスト全体の実行と結果確
              認が自動化されていない

               つまり、このままではJenkinsなどのCI環
               境に組み込みづらい



12年12月5日水曜日
CLIを持つ主なJS処理系
              SpiderMonkey
                C言語実装、Mozillaで保守
              Rhino
                Java実装、Mozillaで保守
                JDK6以降にbundleされている
              Node.js
                サーバーサイドJS実行環境
                処理系はChromeと同じV8エンジン
                同梱されるパッケージ管理のnpmが便利


12年12月5日水曜日
CLIを持つ主なJS処理系
              SpiderMonkey
                C言語実装、Mozillaで保守
              Rhino
                                Rhino+Envjsの話をしようと思ったのですが、
                Java実装、Mozillaで保守
                                Node全盛の今ニッチな気配を感じてるのと
                                Envjsがしばらくメンテされてる雰囲気なし...
                JDK6以降にbundleされている
              Node.js
                サーバーサイドJS実行環境
                処理系はChromeと同じV8エンジン
                同梱されるパッケージ管理のnpmが便利


12年12月5日水曜日
Node.jsのインストール




                     各プラットフォーム向けのインストーラーを取得
                     (ただしCygwinは5.10でサポート外...)




12年12月5日水曜日
QUnitをNodeで動かす




12年12月5日水曜日
QUnit + QUnit-TAP
              QUnit自体に標準出力へテスト結果をレポー
              トする機能がない

              npmモジュールとして公開されている 
              QUnit-TAPを組み合わせるのがオススメ




12年12月5日水曜日
npmインストール
              パッケージ指定してインストール
              $ npm install qunitjs
              $ npm install qunit-tap




              もしくはpackage.json記述してインストール
              $ cat package.json
              {
                "name": "sample-of-tdd",
                "version": "1.0.0",
                "devDependencies": {
                  "qunitjs": "1.10.0",
                  "qunit-tap": "1.2.2"
                }
              }
              $ npm install




12年12月5日水曜日
ソースコードの調整
              以下のソースでブラウザ実行していたとする
              $ tree
              .
              !"" node_modules
              !"" package.json
              !"" runner.html
              !"" src
              #   $"" greeter.js
              $"" test
                  $"" greeter_test.js

              3 directories, 4 files




              // src/Greeter.js
              function Greeter() {
                this.greet = "hello";
              }

              // test/greeter
              module("Greeter");
              test("greetがセットされる", function(assert) {
                var greeter = new Greeter();
                assert.ok(greeter.greet);
              });

12年12月5日水曜日
ソースコードの調整
              ブラウザ/Node両方で動作するように修正
              // src/Greeter.js
              function Greeter() {
                this.greet = "hello";
              }

              if (typeof exports !== "undefined") {
                exports.Greeter = Greeter;
              }

              // test/greeter_test.js
              if (typeof exports !== "undefined") {
                var QUnit = require("qunitjs");
                var qunitTap = require("qunit-tap").qunitTap;
                qunitTap(QUnit, console.log, {noPlan: true});
                QUnit.init();
                QUnit.config.updateRate = 0;
                var Greeter = require("../src/Greeter").Greeter;
              };

              QUnit.module("Greeter");
              QUnit.test("greetがセットされる", function(assert) {
                var greeter = new Greeter();
                assert.ok(greeter.greet);
              });



12年12月5日水曜日
ソースコードの調整
              ブラウザ/Node両方で動作するように修正
              // src/Greeter.js
              function Greeter() {
                this.greet = "hello";
              }

              if (typeof exports !== "undefined") {                exportsオブジェクトの有無で環境を判別
                exports.Greeter = Greeter;
              }                                                    Nodeのモジュール機構に則した形で公開

              // test/greeter_test.js
              if (typeof exports !== "undefined") {
                var QUnit = require("qunitjs");
                var qunitTap = require("qunit-tap").qunitTap;      exportsオブジェクトの有無で環境を判別
                qunitTap(QUnit, console.log, {noPlan: true});
                QUnit.init();                                      QUnitの初期化処理とテスト対象コードの
                QUnit.config.updateRate = 0;                       読み込み
                var Greeter = require("../src/Greeter").Greeter;
              };

              QUnit.module("Greeter");
              QUnit.test("greetがセットされる", function(assert) {        QUnitのグローバル関数は
                var greeter = new Greeter();                       QUnitオブジェクトから参照
                assert.ok(greeter.greet);
              });



12年12月5日水曜日
テスト実行
              テスト結果がTAP形式で出力される
              $ node test/greeter_test.js
              # module: Greeter
              # test: greetがセットされる
              ok 1
              1..1




              proveコマンドを組み合わせることで複数フ
              ァイルの実行&サマリーも可能
              $ prove -e node test/*
              test/greeter_test.js .. ok
              All tests successful.
              Files=1, Tests=1, 1 wallclock secs ( 0.03 usr   0.01 sys +   0.09
              cusr 0.01 csys = 0.14 CPU)
              Result: PASS



12年12月5日水曜日
JasmineをNodeで動かす




12年12月5日水曜日
Jasmine-node
              Jamine-coreとそれを実行するCLIで構成され
              るnpmモジュール

              オプションでJUnit XMLフォーマットで出力
              などCLI向けの拡張がいくつかなされている




12年12月5日水曜日
npmインストール
              コマンドラインツールさえ利用できればよい
              ので -g オプションでシステムにインストール
              $ npm install -g jasmine-node



              ちなみに -g オプションなしでインストール 
              したモジュールのコマンドラインツールは
              node_modules/.bin/ 以下に入る
              $ npm install -g jasmine-node
              $ tree node_modules/.bin/
              node_modules/.bin/
              $"" jasmine-node -> ../jasmine-node/bin/jasmine-node

              0 directories, 1 file



12年12月5日水曜日
ソースコードの調整
              ブラウザ/Node両方で動作するように修正
              // src/Greeter.js
              function Greeter() {
                this.greet = "hello";
              }

              if (typeof exports !== "undefined") {
                exports.Greeter = Greeter;
              }

              // spec/greeter_spec.js
              if (typeof exports !== "undefined") {
                var Greeter = require("../src/greeter").Greeter;
              };

              describe("Greeter", function() {
                it("greetがセットされる", function() {
                  var greeter = new Greeter();
                  expect(greeter.greet).toBeDefined();
                });
              });




12年12月5日水曜日
ソースコードの調整
              ブラウザ/Node両方で動作するように修正
              // src/Greeter.js
              function Greeter() {
                this.greet = "hello";
              }
                                                                   テスト対象コードはQUnitと同じ修正
              if (typeof exports !== "undefined") {
                exports.Greeter = Greeter;
              }

              // spec/greeter_spec.js
              if (typeof exports !== "undefined") {                テスト対象コードのrequire()を追加
                var Greeter = require("../src/greeter").Greeter;   それ以外はブラウザ実行と同様でOK
              };
                                                                   (jasmine-nodeが解決してくれている)
              describe("Greeter", function() {
                it("greetがセットされる", function() {
                  var greeter = new Greeter();
                  expect(greeter.greet).toBeDefined();
                });
              });




12年12月5日水曜日
スペック実行
              スペック結果が出力される
              $ jasmine-node spec
              .

              Finished in 0.014 seconds
              1 test, 1 assertion, 0 failures




              jasmine-nodeの引数にはディレクトリを指定
                 ディレクトリ以下の全スペックファイル
                 (*spec.jsにマッチするファイル)を全て
                 実行してくれる



12年12月5日水曜日
CLI環境での実行
              QUnit、Jasmineともにブラウザ上で実行し
              たソースからテスト(スペック)記述は変更せず
              に最小限の修正で実行することが可能

              しかしまだ、Host ObjectやDOMに依存しな
              いコードしかCLI環境で実行できない

              Node上でDOMを実装したモジュールを利用
              してCLI環境でテスト可能なコードを増やす



12年12月5日水曜日
Node+jsdomを利用した
          DOM依存コードの実行



12年12月5日水曜日
jsdomとは?
              W3CのDOMをJavaScriptで実装したライブ
              ラリ(npmモジュール)
              リモートのHTML/XMLやローカルファイル、
              文字列をパースしてDOMオブジェクトを作成
              これ使えばWebスクレイピングなど簡単
              require("jsdom").env(
                "http://www.mapion.co.jp",
                ["http://code.jquery.com/jquery.js"],
                function (errors, window) {
                  var alt = window.$("h1 img").attr("alt");
                  console.log(alt); // 地図検索マップ マピオン
                }
              );


12年12月5日水曜日
npmインストール
              パッケージ指定してインストール
              $ npm install jsdom




              もしくはpackage.json記述してインストール
              $ cat package.json
              {
                "name": "sample-of-tdd",
                "version": "1.0.0",
                "devDependencies": {
                  "qunitjs": "1.10.0",
                  "qunit-tap": "1.2.2",
                  "jsdom": "0.2.19"
                }
              }
              $ npm install




12年12月5日水曜日
どう利用するか?
              jasmine-nodeにはスペック実行ディレクトリ
              にある「*helpers.js」を読み込んでくれるの
              で、そこに以下ヘルパー関数を定義
              // spec/spec_helpers.js
              var jsdom = require("jsdom");
              global.init_window = function(opt, callback) {
                var html = '<html><body></body></html>';
                jsdom.env((opt && opt.html) || html, function(errors, window) {
                  global.window = window;
                  global.document = window.document;
                  callback(errors);
                });
              };




12年12月5日水曜日
ヘルパー関数の利用
              beforeEachで初期化処理を走らせれば、初期
              化されたwindowとdocumentがグローバルに
              作成される
              // spec/jsdom_spec.js
              describe("jsdomを利用する", function() {
                beforeEach(function(done) {
                  init_window({
                    html: '<html><body><div id="hoge">bar</div></body></html>'
                  }, done);
                });
                it("documentオブジェクトが利用可能", function() {
                  expect(document.getElementById("hoge").innerHTML).toEqual("bar");
                });
              });




              参考:https://github.com/mizchi/sample-node-client-test
12年12月5日水曜日
jsdom利用の留意点
              windowオブジェクトにはXMLHttpRequest
              なども定義されており、ほとんどブラウザ

              しかし、全ての振る舞いが本当のブラウザ上
              オブジェクトと同一である保証はない

              個人的にはPhantomJSを利用するケースのほ
              うが多い




12年12月5日水曜日
ヘッドレスブラウザ
               (PhantomJS)



12年12月5日水曜日
PhantomJSとは?
              GUIのない(ヘッドレスな)ブラウザ   
              JSスクリプトファイルで操作する
              QtWebKitをベースに作られているため
              HTML5/CSS3といったモダンブラウザの機
              能は実装されている
              内部でレンダリングは実行されている   
              API経由で画面キャプチャも取得できる
              var page = require("webpage").create();
              page.open("http://www.mapion.co.jp/", function(state) {
                page.render("mapion.png"); // カレントディレクトリに出力
                phantom.exit();
              });


12年12月5日水曜日
インストール




         Windows/MacOSX/Linux向けのバイナリを
         インストールすれば利用可能




12年12月5日水曜日
ユースケース
              QUnitやJasmineによるテスト実行HTMLの
              Test Runner

              Webページのスクリーンキャプチャツール

              ユーザー操作をエミュレートしたシナリオテ
              ストの実行

              ページリソース(js, css, img)全てを含めたネ
              ットワークモニタリング


12年12月5日水曜日
サンプルコード
                phantomjsソースツリーに含まれる
                examples/pizza.js
              // Find pizza in Mountain View using Yelp

              var page = require('webpage').create(),
                  url = 'http://lite.yelp.com/search?
              find_desc=pizza&find_loc=94040&find_submit=Search';

              page.open(url, function (status) {
                  if (status !== 'success') {
                      console.log('Unable to access network');
                  } else {
                      var results = page.evaluate(function() {
                          var list = document.querySelectorAll('span.address'), pizza = [], i;
                          for (i = 0; i < list.length; i++) {
                              pizza.push(list[i].innerText);
                          }
                          return pizza;
                      });
                      console.log(results.join('n'));
                  }
                  phantom.exit();
              });



12年12月5日水曜日
サンプルコード
                phantomjsソースツリーに含まれる
                examples/pizza.js
              // Find pizza in Mountain View using Yelp

              var page = require('webpage').create(),
                  url = 'http://lite.yelp.com/search?
              find_desc=pizza&find_loc=94040&find_submit=Search';
                                                                    単一のページを読み込んでいるブロック

              page.open(url, function (status) {
                  if (status !== 'success') {
                      console.log('Unable to access network');
                  } else {
                      var results = page.evaluate(function() {
                          var list = document.querySelectorAll('span.address'), pizza = [], i;
                          for (i = 0; i < list.length; i++) {
                              pizza.push(list[i].innerText);
                          }
                          return pizza;
                      });
                      console.log(results.join('n'));
                  }
                  phantom.exit();
              });



12年12月5日水曜日
サンプルコード
                phantomjsソースツリーに含まれる
                examples/pizza.js
              // Find pizza in Mountain View using Yelp

              var page = require('webpage').create(),
                  url = 'http://lite.yelp.com/search?
              find_desc=pizza&find_loc=94040&find_submit=Search';
                                                           ページ内のコンテキストで実行しているブロック
              page.open(url, function (status) {
                  if (status !== 'success') {              (セキュリティ上の理由で別コンテキスト)
                      console.log('Unable to access network');
                                                           DOMツリーから情報を取得している
                  } else {
                      var results = page.evaluate(function() {
                          var list = document.querySelectorAll('span.address'), pizza = [], i;
                          for (i = 0; i < list.length; i++) {
                              pizza.push(list[i].innerText);
                          }
                          return pizza;
                      });
                      console.log(results.join('n'));
                  }
                  phantom.exit();
              });



12年12月5日水曜日
サンプルコード
                phantomjsソースツリーに含まれる
                examples/pizza.js
              // Find pizza in Mountain View using Yelp

              var page = require('webpage').create(),
                  url = 'http://lite.yelp.com/search?
              find_desc=pizza&find_loc=94040&find_submit=Search';

              page.open(url, function (status) {
                  if (status !== 'success') {
                      console.log('Unable to access network');
                  } else {
                      var results = page.evaluate(function() {
                          var list = document.querySelectorAll('span.address'), pizza = [], i;
                          for (i = 0; i < list.length; i++) {
                              pizza.push(list[i].innerText);
                          }
                          return pizza;
                      });
                      console.log(results.join('n'));     取得した情報を標準出力して
                  }
                  phantom.exit();                          ブラウザを終了
              });



12年12月5日水曜日
どうTDDで利用するか?
              Test Runner
                QUnitやJasmineの実行HTMLを開く
                テスト実行を待つ
                結果HTMLをスクレイピング
                PhantomJS実行コンテキストで結果出力

              GitHubページに各フレームワーク毎に作成さ
              れているTest Runnerが紹介されている
               https://github.com/ariya/phantomjs/wiki/Headless-Testing




12年12月5日水曜日
PhantomJS                                   QUnit
              QUnitにbuilt-inされているjsが利用できる
              $ phantomjs node_modules/qunitjs/addons/phantomjs/runner.js qunit.html
              Took 22ms to run 20 tests. 20 passed, 0 failed.




12年12月5日水曜日
pros/cons
              pros
                ブラウザそのもの
                HTML5/CSS3などモダンな実装が動く
                WebKitなのでスマートフォンの標準ブラウ
                ザに近い挙動を期待できる
              cons
                WebKit以外のブラウザがターゲットの場
                合には積極的に利用できない



12年12月5日水曜日
jasmine-gem




12年12月5日水曜日
jasmine-gemとは?
              ブラウザ実行を楽にするヘルパースクリプト
              (SpecRunner.htmlの作成が不要)
              コマンドラインからブラウザによるテスト実
              行をサポート
               仕組みはWebDriver
              railsコマンドのサポート




12年12月5日水曜日
インストール
              gemでインストール
              $ gem install jasmine -v 1.3.0


                 12/3にリリースされたv1.3.1がぶっ壊れて
                 いる?っぽいので v1.3.0 を指定
              初期化
                 rails3プロジェクトの場合
                 $ rails g jasmine:install



                 railsじゃないプロジェクトの場合
                 $ jasmine init



12年12月5日水曜日
initコマンドの出力
              $ jasmine init
              $ tree .
              .
              !"" Rakefile
              !"" public
              #   $"" javascripts
              #        !"" Player.js
              #        $"" Song.js
              $"" spec
                  $"" javascripts
                      !"" PlayerSpec.js
                      !"" helpers
                      #   $"" SpecHelper.js
                      $"" support
                          $"" jasmine.yml

              6 directories, 6 files




12年12月5日水曜日
initコマンドの出力
              $ jasmine init
              $ tree .
              .
              !"" Rakefile
              !"" public
              #   $"" javascripts
              #        !"" Player.js
              #        $"" Song.js            standalone版の初期状態と同じ
              $"" spec
                                              サンプルリソース群が出力されている
                  $"" javascripts
                      !"" PlayerSpec.js
                      !"" helpers
                      #   $"" SpecHelper.js
                      $"" support
                          $"" jasmine.yml

              6 directories, 6 files




12年12月5日水曜日
initコマンドの出力
              $ jasmine init
              $ tree .
              .
                                      Rakefileとjasmine.ymlがstatdalone版に
              !"" Rakefile
                                      含まれていなかったリソース
              !"" public
              #   $"" javascripts
              #        !"" Player.js
              #        $"" Song.js
              $"" spec
                  $"" javascripts
                      !"" PlayerSpec.js
                      !"" helpers
                      #   $"" SpecHelper.js
                      $"" support
                          $"" jasmine.yml

              6 directories, 6 files




12年12月5日水曜日
Rakeタスクの実行
              rake -T コマンドで確認
              $ rake -T
              rake jasmine      # Run specs via server
              rake jasmine:ci   # Run continuous integration
              tests



              rake jasmineでサーバーが起動、表示された
              URLにアクセスするとテスト実行できる
              $ rake jasmine
              your tests are here:
                http://localhost:8888/



              ポート指定は環境変数JASMINE_PORT
              $ JASMINE_PORT=1234 rake jasmine
              your tests are here:
                http://localhost:1234/

12年12月5日水曜日
読み込むjsの設定
              jasmine.ymlで指定可能、ルールや書き方はコ
              メントに記載されている
              # src_files
              #
              # Return an array of filepaths relative to src_dir to include before jasmine specs.
              # Default: []
              #
              # EXAMPLE:
              #
              # src_files:
              #   - lib/source1.js
              #   - lib/source2.js
              #   - dist/**/*.js
              #
              src_files:
                  - public/javascripts/**/*.js

              # stylesheets
              #

               :
              (省略)
               :




12年12月5日水曜日
$ rake jasmine:ci
              WebDriverを利用してブラウザ実行も自動化
              $ rake jasmine:ci


              デフォルトではFirefoxが起動
              他のブラウザ起動は環境変数
              JASMINE_BROWSERで指定を行う
              $ JASMINE_BROWSER=chrome rake jasmine:ci


                 指定可能値                                 ie, chrome, android, iphone, opera
                 see: https://github.com/vertis/selenium-webdriver/blob/master/lib/selenium/webdriver/common/driver.rb#L25



                 chrome, android, iphone, operaのdriver
                 詳細はSeleniumのwikiページにご参照

12年12月5日水曜日
複数ブラウザで実行する
                自動テストツール



12年12月5日水曜日
自動テストツール/テストランナー


                       Browser                    Capturing
                                   Unit Testing
                      Automation                  Browser


                                                              そもそもユニットテスト用
        Selenium         ○              -            -            ではない




                                                                 ユニットテスト +
       JsTestDriver       -            ○             ○          ブラウザキャプチャ



                                                                 ユニットテスト +
        BusterJS          -            ○             ○          ブラウザキャプチャ
                                                                   Node製



                                                              先月(2012/11)公開された
        Testacular        -             -            ○         ブラウザキャプチャのみ
                                                                    Node製




12年12月5日水曜日
ブラウザキャプチャ?
              サーバーにブラウザを接続させコネクション
              を維持、サーバー側でコマンドを実行するこ
              とでテスト実行と結果サマリーを複数ブラウ
              ザで一気に行う方法

              正式名称は知りません




12年12月5日水曜日
Testacularを使う
              先月末(2012/11)にGoogleからオープンソー
              ス化して公開されたばかり!

              Node製でSoket.ioを利用してブラウザキャプ
              チャを行うシンプルなツール

              ユニットテストは含まれていない、既存のテ
              スト資産(Jasmine, Mochaなど)を活用する




12年12月5日水曜日
インストール
                インストールとコマンドラインオプションの
                確認
              $ npm install -g testacular
              $ testacular --help
              Testacular - Spectacular Test Runner for JavaScript.

              Usage:
                testacular <command>

              Commands:
                start [<configFile>] [<options>] Start the server / do single run.
                init [<configFile>] Initialize a config file.
                run [<options>] Trigger a test run.

              Run --help with particular command to see its description and available options.

              Options:
                --help      Print usage and options.
                --version   Print current version.




12年12月5日水曜日
前準備
              テストリソースをjasmine-gemで用意
              $ jasmine init
              $ tree .
              .
              !"" Rakefile
              !"" public
              #   $"" javascripts
              #        !"" Player.js
              #        $"" Song.js
              $"" spec
                  $"" javascripts
                      !"" PlayerSpec.js
                      !"" helpers
                      #   $"" SpecHelper.js
                      $"" support
                          $"" jasmine.yml

              6 directories, 6 files



12年12月5日水曜日
設定ファイルの作成
              initコマンドで対話的に作成してくれる
              $ testacular init

              Which testing framework do you want to use ?
              Press tab to list possible options. Enter to move to the next question.
              > jasmine

              Do you want to capture a browser automatically ?
              Press tab to list possible options. Enter empty string to move to the next question.
              > Chrome
              > Firefox
              > Safari
              >

              Which files do you want to test ?
              You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
              Enter empty string to move to the next question.
              > public/**/*.js
              > spec/**/*.js
              >

              Any files you want to exclude ?
              You can use glob patterns, eg. "**/*.swp".
              Enter empty string to move to the next question.
              >

              Do you want Testacular to watch all the files and run the tests on change ?
              Press tab to list possible options.
              > yes

              Config file generated at "/Users/kozy/js-dev/testacular/testacular.conf.js".

12年12月5日水曜日
設定ファイルの作成
              initコマンドで対話的に作成してくれる
              $ testacular init

              Which testing framework do you want to use ?
                                       どのテストフレームワークを利用するか?
              Press tab to list possible options. Enter to move to the next question.
              > jasmine
                                      デフォルトでJasmineかMochaが選択可
              Do you want to capture a browser automatically ?
              Press tab to list possible options. Enter empty string to move to the next question.
              > Chrome
              > Firefox                サーバー起動時に接続するブラウザ
              > Safari                 起動後に手動で接続することも可能
              >

              Which files do you want to test ?
              You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
              Enter empty string to move to the next question.
              > public/**/*.js         テスト実行HTMLに読み込むjsファイル
              > spec/**/*.js
              >                        globパターンで指定可能
              Any files you want to exclude ?
              You can use glob patterns, eg. "**/*.swp".
              Enter empty string to move to the next question.
              >                        逆に読み込まないjsファイルを指定
              Do you want Testacular to watch all the files and run the tests on change ?
              Press tab to list possible options.
              > yes                    ファイル更新を検知して再実行するか
              Config file generated at "/Users/kozy/js-dev/testacular/testacular.conf.js".

12年12月5日水曜日
設定ファイルを修正
              何故かパス設定がうまく動かない...
              basePathを修正する
                1   // Testacular configuration
                2   // Generated on Wed Dec 05 2012 23:01:06 GMT+0900 (JST)
                3
                4
                5   // base path, that will be used to resolve files and exclude
                6   basePath = '../../../../..';
                7
                8
                9   // list of files / patterns to load in the browser
               10   files = [
               11      JASMINE,
               12      JASMINE_ADAPTER,
               13      'public/**/*.js',
               14      'spec/**/*.js'
               15   ];
                :




12年12月5日水曜日
設定ファイルを修正
              何故かパス設定がうまく動かない...
              basePathを修正する
                1   // Testacular configuration
                2   // Generated on Wed Dec 05 2012 23:01:06 GMT+0900 (JST)
                3
                4
                5   // base path, that will be used to resolve files and exclude
                                       内部ではrequire('path').resolve(basePath, files[i])で
                6   basePath = '';
                7                      解決するため正しいパスが得られない... 空文字列に変更
                8
                9   // list of files / patterns to load in the browser
               10   files = [
               11      JASMINE,
               12      JASMINE_ADAPTER,
               13      'public/**/*.js',
               14      'spec/**/*.js'
               15   ];
                :




12年12月5日水曜日
実行!
              以下コマンドでサーバーが起動
              $ testacular start


              設定ブラウザも起動しキャプチャされる
                 もちろん手動で接続してキャプチャさせる
                 ことも可能(スマホブラウザなど)
              読み込みファイルの更新検知、キャプチャ済
              みブラウザのリロード、runコマンドの送信で
              ユニットテストが各ブラウザで自動実行



12年12月5日水曜日
使ってみた感じ
              設定ファイルの自動生成など、導入する面倒
              くささがまったくない

              テスト実行がかなり早い、ファイル更新での
              自動実行もサクサク動く

              よくたびたびtestacular経由で起動した
              Chromeが終了ミス?って親なしプロセスに

              まだ粗い感じもあるが、かなり使えるツール
              なのでは?

12年12月5日水曜日
CI
              (Jenkins)



12年12月5日水曜日
jarのダウンロード




12年12月5日水曜日
jarから直接起動
              ちゃんと運用する時はTomcatなどアプリケー
              ションコンテナにデプロイしてください

              以下コマンドで8080ポートで起動
              $ java -jar jenkins.war




12年12月5日水曜日
http://localhost:8080/

                          新規ジョブ作成はこっち




     システムの設定はここ
     Gitプラグインをまず入れる




12年12月5日水曜日
Gitプラグイン取得




12年12月5日水曜日
Gitプラグイン取得




                チェックを入れて再起動




12年12月5日水曜日
再起動して新規ジョブ作成


                   フリースタイルでOK




12年12月5日水曜日
プロジェクト設定(1)




                 テストなのでローカルパスで




12年12月5日水曜日
プロジェクト設定(2)



                  ビルド手順にテスト実行スクリプトを記述




                  jasmine-nodeのjunitreportはデフォルトで
                  reports以下に結果を出力する




12年12月5日水曜日
ビルド実行


              手動で実行




12年12月5日水曜日
結果が確認できる




12年12月5日水曜日
ビルド実行URL
                以下URLでビルドが実行される

                      [プロジェクトURL]/build?delay=0sec


                Gitならコミットフックを仕込むと幸せになれる
              $ echo 'curl "http://localhost:8080/job/your_project/build?delay=0sec"'>.git/hooks/pre-commit
              $ chmod +x .git/hooks/pre-commit




12年12月5日水曜日
まとめ




12年12月5日水曜日
まとめ
              ブラウザ上で実行するユニットテストツール
              QUnitとJasmineを紹介しました
              QUnitとJasmineをベースにTDDで活用でき
              そうなCLI環境やヘッドレスブラウザの利用方
              法を紹介しました
              ブラウザキャプチャによる複数ブラウザでの
              ユニットテスト同時実行を紹介しました
              CIをJenkinsで行うための簡単な設定例を紹介
              しました

12年12月5日水曜日
実は...
              BusterJSを使えば
               ブラウザ上でユニットテスト出来ます
               Nodeでユニットテスト出来ます
               複数ブラウザの同時実行も出来ます
               JenkinsなどでCI導入も可能です
              BusterJSの万能感がハンパないです



12年12月5日水曜日
今後特にウォッチしたい
              BusterJS

              Testacular




12年12月5日水曜日
以上
              ありがとうございました



12年12月5日水曜日

More Related Content

JS開発におけるTDDと自動テストツール利用の勘所

  • 1. JS開発における TDDと自動テスト ツール利用の勘所 2012.12.06 株式会社マピオン 中村 浩士 12年12月5日水曜日
  • 2. 自己紹介 中村 浩士 ( @kozy4324 ) 株式会社マピオン所属 主にWebアプリのフロントエンド開発 JavaScript, ActionScript 12年12月5日水曜日
  • 4. Mapion 地図情報検索サイト 月間7600万PV、1200万UU 全文検索エンジンSolrを利用した900万件超 のスポット情報を検索できる電話帳/地図面 その他位置情報コンテンツやナビサービス 2008年からスマートフォンサイトにも注力 Android、iOSのネイティブアプリ開発も 12年12月5日水曜日
  • 5. 地図を使ったWebアプリ よく開発しています 12年12月5日水曜日
  • 6. 今回 話すること TDDの基本となるJSユニットテストツールの 使い方 WebアプリでのTDDを意識した設計について (少しだけ) 様々なツールを利用してTDD/自動テストの効 率化を試みる話 12年12月5日水曜日
  • 7. 話しないこと TDD自体について 詳細なやり方、あるべき論 WebアプリでのTDDベストプラクティス 僕はまだその答えに辿り着いていないです... 「テスト駆動JavaScript」が良書なので、それを読みましょう Webアプリに対するシナリオベースの     自動テストについて ユーザー操作をエミュレートしてWebアプ リ全体の振る舞いを自動でテストする方法 12年12月5日水曜日
  • 8. アジェンダ ブラウザ上で実行するJSユニットテストツール 各ツール比較 使い方&コードサンプル WebアプリのTDDを意識した設計について TDDや自動テストで活用できる各種ツール コマンドライン環境 ヘッドレスブラウザ 自動テストツール CI環境 12年12月5日水曜日
  • 9. ブラウザ上で実行する JSユニットテストツール 12年12月5日水曜日
  • 10. TDDとは Test-Driven Development(テスト駆動開発) 分析技法、設計技法( テスト技法) 正しく動くソフトウェアを確実に作り上げるため のテクニック 進め方 1. テストを書く(テストファースト) 2. テストをパスする最低限の実装を行う 3. テストのパスを保持したままコードの重複を除 去する(リファクタリング) 4. 1∼3を短いスパンで繰り返す 12年12月5日水曜日
  • 11. TDDの効果 書いたプログラムに対する即座のフィードバック 要求の理解の促進 リファクタリングの支援、クリーンコードの促進 自動テストによるデグレード検知 プログラマが持つ不安の解消 心の健康をもたらす :) 12年12月5日水曜日
  • 12. JSユニットテストツール JsUnit YUI Test Google Closure Tools QUnit Jasmine Mocha Vows (etc...) 12年12月5日水曜日
  • 13. JSユニットテストツール JsUnit YUI Test Google Closure Tools QUnit 自分がよく利用するのはこの4つ QUnit, Jasmine, Mochaはブラウザ上で実行可能 Jasmine Mocha Vows (etc...) 12年12月5日水曜日
  • 14. ざっくり比較 非同期 スタイル ブラウザ実行 CLI実行 サポート シンプル QUnit フラット ○ △ ○ ブラウザ実行に最適 Rubyist向け Jasmine BDD ○ ○ ○ Jasmine-gemくそ便利 BDD, TDD, Exports, Nodeモジュール Mocha フラットが選べる ○ ○ ○ フレキシブル Nodeモジュール Vows Exports ○ ○ Nodeの非同期処理テストが スマートに書ける 12年12月5日水曜日
  • 16. ケース別 プロジェクトへの導入が目的 シンプルなQUnitがオススメ Ruby / Ruby on Railsがメインの領域な人 Jasmineがオススメ CLI得意 / Node.jsもやりたい! Mocha, Vowsがいいのでは? 12年12月5日水曜日
  • 17. QUnitとJasmineの 使い方を紹介 12年12月5日水曜日
  • 19. QUnitとは? ブラウザ上での実行を想定したJSユニットテ ストフレームワーク jQueryの開発に利用されている シンプルさが特徴 MITライセンス 現在のリリースバージョンは v1.10.0 12年12月5日水曜日
  • 20. http://qunitjs.com サイトからjsとcssをダウンロードして利用可能 12年12月5日水曜日
  • 21. npmインストールの場合 パッケージ指定してインストール $ npm install qunitjs $ ls node_modules/qunitjs/qunit/ qunit.css!qunit.js もしくはpackage.json記述してインストール $ cat package.json {   "name": "sample-of-tdd",   "version": "1.0.0",   "devDependencies": {     "qunitjs": "1.10.0"   } } $ npm install 12年12月5日水曜日
  • 22. HTML記述例 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>objects</title> qunit.js, qunit.css, テスト対象のjsファイル, テスト <link rel="stylesheet" href="qunit.css"> コードを記述したjsファイルの4リソースを読み込む <script src="qunit.js"></script> titleを設定することを強く推奨 <script src="objects.js"></script> <script src="objects_test.js"></script> </head> <body> <div id="qunit"></div> id="qunit"の要素に結果が出力される <div id="qunit-fixture"></div> id="qunit-fixture"はテスト実行の度に初期状態に復元 </doby> されるのでDOMに依存したテストの場合に利用できる </html> 12年12月5日水曜日
  • 23. テストの基本構造 module("Object"); test("#methodA", function(assert) { module → test → アサーションの3階層   assert.ok(true, "some messages"); }); test("#methodB", function(assert) {   assert.ok(true, "some messages");   assert.ok(true, "some messages"); }); module("Array"); 次のmodule関数を呼ぶまでのtest関数がグルーピングされる test("#methodA", function(assert) {   assert.ok(true, "some messages"); (関数をネストする形ではない → フラットな形式)   assert.ok(true, "some messages"); }); test("#methodB", function(assert) {   assert.ok(true, "some messages");   assert.ok(true, "some messages");   assert.ok(true, "some messages"); }); 12年12月5日水曜日
  • 24. アサーション ok(state[, message]) equal(actual, expected[, message]) notEqual(actual, expected[, message]) deepEqual(actual, expected[, message]) notDeepEqual(actual, expected[, message]) strictEqual(actual, expected[, message]) notStrictEqual(actual, expected[, message]) throws(block, expected[, message]) CommonJS Unit Testingの仕様に追従している 12年12月5日水曜日
  • 25. setup/teardown module("Object", {   setup: function() {     this.object = new MyObject(); module()の第2引数のオブジェクトにsetupとteardownを設定   },   teardown: function() { で、テスト実行毎の前処理/後処理が行える     // do something...   } }); test("#methodA", function(assert) {   assert.ok(this.object.methodA()); thisでスコープが共有(ただしテスト毎のthisは別オブジェクト) }); test("#methodB", function(assert) {   assert.ok(this.object.methodB()); }); module("Array", {   setup: function() {     this.array = new MyArray(); setup/teardownはmodule単位で別に設定できる   } }); test("#methodA", function(assert) {   assert.ok(this.array.methodA()); }); test("#methodB", function(assert) {   assert.ok(this.array.methodB()); }); 12年12月5日水曜日
  • 26. expect() test("#forEach with 1 item", 1, function(assert) {   [1].forEach(function(){ テスト内のアサーション数をチェック     assert.ok(true);   }); コールバック振る舞いの確認に利用可能 }); test()の引数に指定もしくは test("#forEach with 2 items", function(assert) { expect()関数で指定する   expect(2);   [1,2].forEach(function(){     assert.ok(true);   }); }); ただし、expect()だけでのコールバック振る舞いテストは貧弱なので 複雑なケースはSinon.jsを利用したほうがよい 12年12月5日水曜日
  • 27. 非同期処理のテスト test("asyncTest A", function(assert) {   expect(1); stop()で次テストの実行を保留   setTimeout(function() { start()で保留を解除する     assert.ok(true);     start();   }, 1000);   stop(); }); asyncTest("asyncTest B", function(assert) { test() → asyncTest()とすることで   expect(1); stop()を省略できる   setTimeout(function() {     assert.ok(true);     start();   }, 1000); }); 12年12月5日水曜日
  • 29. 実行結果 onでグローバルへの変数汚染を チェックするモードで再実行 モジュールでの絞り込み実行も可能 リストをクリックすると詳細を開閉 (エラー時は最初から開いている) Rerun選択 or ダブルクリックで 特定テストのみ再実行 再実行時はfailしたテストから実行する(sessionStorage利用してる) その仕様を知らずに順番に依存したテストを書くと死ねます... 12年12月5日水曜日
  • 32. Jasmineとは? RubyのRSpecライクな記法のBDD(ビヘイ ビア駆動開発)フレームワーク 豊富なExpectationsとMatchers (QUnitで言うアサーション) spyによるTest Double(テスト代役) プラガブルなReporter MITライセンス 現在のリリースバージョンは v1.3.0 12年12月5日水曜日
  • 33. https://github.com/pivotal/jasmine GitHubプロジェクトページのDownloadsにある zipファイルをダウンロードして解凍 12年12月5日水曜日
  • 34. 補足:関連プロダクト GitHub: pivotal/jasmine-gem RubyGems: jasmine npm: - 依存 RackやSeleniumを含めた実行ヘルパー GitHub: pivotal/jasmine RubyGems: jasmine-core npm: - JavaScriptのフレームワーク部分 GitHub: mhevery/jasmine-node ダウンロードした 依存 RubyGems: - standalone版はコレ npm: jasmine-node Nodeで実行するためのCLIラッパー jasmine-gemやjasmine-nodeについては後半で 12年12月5日水曜日
  • 35. zipファイルの中身 $ tree . !"" SpecRunner.html htmlがすでにサンプルとして !"" lib 動くものになっている #   $"" jasmine-1.3.0 #   !"" MIT.LICENSE #   !"" jasmine-html.js #   !"" jasmine.css #   $"" jasmine.js !"" spec #   !"" PlayerSpec.js #   $"" SpecHelper.js $"" src     !"" Player.js     $"" Song.js 4 directories, 9 files 12年12月5日水曜日
  • 36. SpecRunner.htmlの中身 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head>   <title>Jasmine Spec Runner</title>   <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.0/jasmine_favicon.png">   <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.0/jasmine.css">   <script type="text/javascript" src="lib/jasmine-1.3.0/jasmine.js"></script>   <script type="text/javascript" src="lib/jasmine-1.3.0/jasmine-html.js"></script>   <!-- include source files here... -->   <script type="text/javascript" src="src/Player.js"></script>   <script type="text/javascript" src="src/Song.js"></script>   <!-- include spec files here... -->   <script type="text/javascript" src="spec/SpecHelper.js"></script>   <script type="text/javascript" src="spec/PlayerSpec.js"></script>   <script type="text/javascript">     (function() {       var jasmineEnv = jasmine.getEnv();       jasmineEnv.updateInterval = 1000; テスト対象のコードとスペックファイル(テストコード)を : それぞれ追加していけばよい (がっつり初期化処理が書いてあるので省略) :     })();   </script> </head> <body> </body> </html> 12年12月5日水曜日
  • 37. テストの基本構造 describe("Array", function() {   describe(".isArray", function() {     it("should return true when called with an array", function() {       expect(Array.isArray([])).toBeTruthy();     });   });   describe("(has no item)", function() {     describe("#join", function() {       it("should return an empty string", function() {         expect([].join()).toEqual("");       });     });   }); }); describe → it → expectationsの3階層 describeはネストして記述することが可能 ex) describe → describe → it → expectations 12年12月5日水曜日
  • 38. Matchers expect(x).toEqual(y) expect(x).not.toEqual(y) expect(x).toBe(y) notで否定のMatcherとなる expect(x).toMatch(pattern) expect(x).toBeDefined() toBeは === による等値チェック expect(x).toBeUndefined() expect(x).toBeNull() expect(x).toBeNaN() expect(x).toBeTruthy() expect(x).toBeFalsy() expect(x).toContain(y) expect(x).toBeLessThan(y) expect(x).toBeGreaterThan(y) expect(x).toBeCloseTo(y, precision) expect(function(){fn();}).toThrow(e) expect(spy).toHaveBeenCalled() expect(spy).toHaveBeenCalledWith(arguments) and more ... 詳しくはGitHubのwikiページを参照 12年12月5日水曜日
  • 39. beforeEach/afterEach describe("Object", function() { var object;   beforeEach(function() {     object = new MyObject(); describeのスコープ内でbeforeEach/afterEachを設定すること   });   afterEach(function() { で、 テスト実行毎の前処理/後処理が行える     // do something...   }); describe("#methodA", function() {    it("should be ok", function() { expect(object.methodA()).toBeTruthy(); }); }); describe("#methodB", function() {    it("should be ok", function() { expect(object.methodB()).toBeTruthy(); }); }); describe("(context)", function() { beforeEach(function() { object.someMethod(); }); describe("#methodC", function() {    it("should be ok", function() { expect(object.methodC()).toBeTruthy(); }); }); }); ネストしたdescribeそれぞれで設定した場合、親子関係の順でコールバックされる }); 親のbeforeEach → 子のbeforeEach → 子のafterEach → 親のafterEach 12年12月5日水曜日
  • 40. spy it("should be called", function() { spyOnメソッドでオブジェクトの var obj = {method: function() {}}; 特定メソッドをスパイ化 spyOn(obj, "method"); obj.method(); spy用のMatcherが用意されている expect(obj.method).toHaveBeenCalled(); }); 詳しくはGitHubのwikiページを参照 test("should be called", function() { jasmine.createSpy()関数でスパイ化 var spy = jasmine.createSpy(); spy(); された関数オブジェクトを作成 expect(spy).toHaveBeenCalled(); }); Jasmineのspyオブジェクトは強力で十分な機能を 有しているが、Sinon.jsのほうが高機能 12年12月5日水曜日
  • 41. 非同期処理のテスト it("should be async", function() {   runs(function() { 非同期処理ブロックはruns()で定義される     expect(true).toBeTruthy();   }); waits()で次のブロックの実行を、指定した   waits(500); ミリ秒間保留する   var spy = jasmine.createSpy();   runs(function() {     setTimeout(spy, 1000);   });   waitsFor(function() { waitsFor()はコールバックがtrueを返す     return spy.callCount > 0;   }); まで、次のブロック実行を保留する   runs(function() {     expect(true).toBeTruthy();   }); }); 12年12月5日水曜日
  • 43. 実行結果 spec毎のpass or failの結果 specのテキストをクリックすると、 該当スペックのみを再実行 12年12月5日水曜日
  • 46. module("Array"); test(".isArray", function(assert) {   assert.ok(Array.isArray([]), "Arrayでtrue"); }); module("Array.prototype", {   setup: function() {     this.array = [1,2,3];     this.empty_array = [];   } }); test("#concat", function(assert) {   assert.deepEqual(this.array.concat(), [1,2,3], "引数なしは配列のコピーを返す");   assert.deepEqual(this.array.concat(4), [1,2,3,4], "引数を末尾に連結した配列を返す");   assert.deepEqual(this.array.concat(4,5), [1,2,3,4,5], "引数は可変長に指定できる");   assert.deepEqual(this.array.concat([4,5]), [1,2,3,4,5], "配列は展開されて連結される"); }); test("#join", function(assert) {   assert.equal(this.array.join(), "1,2,3", "カンマで連結された文字列を返す");   assert.equal(this.array.join("-"), "1-2-3", "引数の文字列で連結された文字列を返す");   assert.equal(this.empty_array.join(), "", "要素がない配列は空文字列を返す");   assert.equal(this.empty_array.join("-"), "", "セパレーターを指定しても空文字列"); }); test("#pop", function(assert) {   assert.equal(this.array.pop(), 3, "末尾の要素を返す");   assert.deepEqual(this.array, [1,2], "戻り値の要素が削除される");   assert.equal(this.empty_array.pop(), undefined, "空配列はundefinedを返す"); }); test("#push", function(assert) {   assert.equal(this.empty_array.push(1), 4, "引数の要素を追加した後のサイズを返す");   assert.deepEqual(this.empty_array, [1,2,3,1], "要素が配列に追加される");   assert.equal(this.empty_array.push(2,3), 6, "引数の要素を追加した後のサイズを返す");   assert.deepEqual(this.empty_array, [1,2,3,1,2,3], "全ての要素が配列に追加される"); }); 12年12月5日水曜日
  • 47. describe("Array", function() {   describe(".isArray", function() {     it("should return true when called with an array", function() {       expect(Array.isArray([])).toBeTruthy();     });   });   describe("(has 3 items)", function() {     var array;     beforeEach(function() {       array = [1,2,3];     });     describe("#concat", function() {       it("should return an array of own copy when called with no argument", function() {         expect(array.concat()).toEqual([1,2,3]);       });       it("should return an array including passed argument", function() {         expect(array.concat(4)).toEqual([1,2,3,4]);         expect(array.concat(4,5)).toEqual([1,2,3,4,5]);       });       it("should return an array including passed argument with array splatting", function() {         expect(array.concat([4,5])).toEqual([1,2,3,4,5]);       });     });     describe("#join", function() {       it("should return a string joined items with comma when called with no argument", function() {         expect(array.join()).toBe("1,2,3");       });       it("should return a string joined items with passed argument", function() {         expect(array.join("-")).toBe("1-2-3");       });     });     describe("#pop", function() {       it("should return and remove the last item", function() {         expect(array.pop()).toBe(3);         expect(array).toEqual([1,2]);       });     });     describe("#push", function() {       it("should add arguments into own, and return own size", function() {         expect(array.push(1)).toBe(4);         expect(array).toEqual([1,2,3,1]);         expect(array.push(2,3)).toBe(6);         expect(array).toEqual([1,2,3,1,2,3]);       });     });   });   describe("(has no item)", function() {     var array;     beforeEach(function() {       array = [];     });     describe("#join", function() {       it("should return an empty string", function() {         expect(array.join()).toBe("");         expect(array.join("-")).toBe("");       });     });     describe("#pop", function() {       it("should return undefined", function() {         expect(array.pop()).toBeUndefined();       });     });   }); }); 12年12月5日水曜日
  • 48. QUnit vs Jasmine ブラウザ上の実行では基本機能は同等 記述スタイルの違い、好みの問題 QUnitはボキャブラリーが絞られるので  簡潔にならざるを得ない、表現力は劣る Jasmineは構造化しやすいがネストが深く なりがち(平均3∼5)、記述量も多め 12年12月5日水曜日
  • 49. WebアプリのTDDを 意識した設計について 12年12月5日水曜日
  • 50. TDDやりづらい実装 host objectに強依存 host objectとは実行環境から提供されるオ ブジェクト ex) window, navigator, location, etc... DOMオブジェクトに強依存 12年12月5日水曜日
  • 51. host objectに強依存 location.searchのクエリーストリングをオブ ジェクトに変換する関数の実装 function parseQuery() {   var obj = {}, kvs = location.search.substring(1).split("&");   kvs.forEach(function(kv){obj[kv.split("=")[0]]=kv.split("=")[1]});   return obj; } query = parseQuery(); locationオブジェクトへの参照を外に出すだ けでユニットテストは書きやすくなる function parseQuery(search) {   var obj = {}, kvs = search.substring(1).split("&");   kvs.forEach(function(kv){obj[kv.split("=")[0]]=kv.split("=")[1]});   return obj; } query = parseQuery(location.search); 12年12月5日水曜日
  • 52. host objectに強依存 どうしても引数を指定しないI/Fを作成したい のであれば、ラッパー関数で分離 function parseQuery() {   return _parseQuery(location.search); } function _parseQuery(search) {   var obj = {}, kvs = search.substring(1).split("&");   kvs.forEach(function(kv){obj[kv.split("=")[0]]=kv.split("=")[1]});   return obj; } query = parseQuery(); 12年12月5日水曜日
  • 53. DOMオブジェクトに強依存 例えばjQueryでありがちがコード $(function(){   $("div li .button").click(function(){     $("div .contents").html("<span>"+$(this).data("mydata")+"</span>");   }) }) DOMに依存することで発生する問題点 DOM要素が存在しないと実行できない DOM操作に対する副作用の検証(アサー ション)が大抵のケースで非常に難しい UIに伴って変更されやすいHTML構造に依 存してしまう(上記ではセレクター部分) 12年12月5日水曜日
  • 54. DOMオブジェクトに強依存 問題に対するアプローチ DOMに依存しない部分を分離する $(function(){   $("#button").click(function(){     clickHandler($("#contents"), $(this).data("mydata"));   }) }) function clickHandler(elm, data) {   elm.html("<span>"+data+"</span>"); } DOM操作の振る舞いのみをテストする it("should call html() of passed element", function() {   var fakeObj = {html: jasmine.createSpy()};   clickHandler(fakeObj, "hoge");   expect(fakeObj.html).toHaveBeenCalledWith("<span>hoge</span>"); }); 可能であればHTML構造に依存しない   セレクタ(idセレクタなど)に変更 12年12月5日水曜日
  • 55. host object/DOMへの 依存を分離した設計で Nodeなどの別環境でも ユニットテストが書ける 12年12月5日水曜日
  • 56. TDDや自動テストで 活用できる各種ツール 12年12月5日水曜日
  • 57. コマンドライン環境 (CLI) 12年12月5日水曜日
  • 58. CLIでTDDする動機 ブラウザ実行での コード修正→保存→アプリ ケーション切替→ブラウザ再読み込み、この 手順が煩雑 ブラウザ実行ではテスト全体の実行と結果確 認が自動化されていない つまり、このままではJenkinsなどのCI環 境に組み込みづらい 12年12月5日水曜日
  • 59. CLIを持つ主なJS処理系 SpiderMonkey C言語実装、Mozillaで保守 Rhino Java実装、Mozillaで保守 JDK6以降にbundleされている Node.js サーバーサイドJS実行環境 処理系はChromeと同じV8エンジン 同梱されるパッケージ管理のnpmが便利 12年12月5日水曜日
  • 60. CLIを持つ主なJS処理系 SpiderMonkey C言語実装、Mozillaで保守 Rhino Rhino+Envjsの話をしようと思ったのですが、 Java実装、Mozillaで保守 Node全盛の今ニッチな気配を感じてるのと Envjsがしばらくメンテされてる雰囲気なし... JDK6以降にbundleされている Node.js サーバーサイドJS実行環境 処理系はChromeと同じV8エンジン 同梱されるパッケージ管理のnpmが便利 12年12月5日水曜日
  • 61. Node.jsのインストール 各プラットフォーム向けのインストーラーを取得 (ただしCygwinは5.10でサポート外...) 12年12月5日水曜日
  • 63. QUnit + QUnit-TAP QUnit自体に標準出力へテスト結果をレポー トする機能がない npmモジュールとして公開されている  QUnit-TAPを組み合わせるのがオススメ 12年12月5日水曜日
  • 64. npmインストール パッケージ指定してインストール $ npm install qunitjs $ npm install qunit-tap もしくはpackage.json記述してインストール $ cat package.json {   "name": "sample-of-tdd",   "version": "1.0.0",   "devDependencies": {     "qunitjs": "1.10.0",     "qunit-tap": "1.2.2"   } } $ npm install 12年12月5日水曜日
  • 65. ソースコードの調整 以下のソースでブラウザ実行していたとする $ tree . !"" node_modules !"" package.json !"" runner.html !"" src #   $"" greeter.js $"" test     $"" greeter_test.js 3 directories, 4 files // src/Greeter.js function Greeter() {   this.greet = "hello"; } // test/greeter module("Greeter"); test("greetがセットされる", function(assert) {   var greeter = new Greeter();   assert.ok(greeter.greet); }); 12年12月5日水曜日
  • 66. ソースコードの調整 ブラウザ/Node両方で動作するように修正 // src/Greeter.js function Greeter() {   this.greet = "hello"; } if (typeof exports !== "undefined") {   exports.Greeter = Greeter; } // test/greeter_test.js if (typeof exports !== "undefined") {   var QUnit = require("qunitjs");   var qunitTap = require("qunit-tap").qunitTap;   qunitTap(QUnit, console.log, {noPlan: true});   QUnit.init();   QUnit.config.updateRate = 0;   var Greeter = require("../src/Greeter").Greeter; }; QUnit.module("Greeter"); QUnit.test("greetがセットされる", function(assert) {   var greeter = new Greeter();   assert.ok(greeter.greet); }); 12年12月5日水曜日
  • 67. ソースコードの調整 ブラウザ/Node両方で動作するように修正 // src/Greeter.js function Greeter() {   this.greet = "hello"; } if (typeof exports !== "undefined") { exportsオブジェクトの有無で環境を判別   exports.Greeter = Greeter; } Nodeのモジュール機構に則した形で公開 // test/greeter_test.js if (typeof exports !== "undefined") {   var QUnit = require("qunitjs");   var qunitTap = require("qunit-tap").qunitTap; exportsオブジェクトの有無で環境を判別   qunitTap(QUnit, console.log, {noPlan: true});   QUnit.init(); QUnitの初期化処理とテスト対象コードの   QUnit.config.updateRate = 0; 読み込み   var Greeter = require("../src/Greeter").Greeter; }; QUnit.module("Greeter"); QUnit.test("greetがセットされる", function(assert) { QUnitのグローバル関数は   var greeter = new Greeter(); QUnitオブジェクトから参照   assert.ok(greeter.greet); }); 12年12月5日水曜日
  • 68. テスト実行 テスト結果がTAP形式で出力される $ node test/greeter_test.js # module: Greeter # test: greetがセットされる ok 1 1..1 proveコマンドを組み合わせることで複数フ ァイルの実行&サマリーも可能 $ prove -e node test/* test/greeter_test.js .. ok All tests successful. Files=1, Tests=1, 1 wallclock secs ( 0.03 usr 0.01 sys + 0.09 cusr 0.01 csys = 0.14 CPU) Result: PASS 12年12月5日水曜日
  • 70. Jasmine-node Jamine-coreとそれを実行するCLIで構成され るnpmモジュール オプションでJUnit XMLフォーマットで出力 などCLI向けの拡張がいくつかなされている 12年12月5日水曜日
  • 71. npmインストール コマンドラインツールさえ利用できればよい ので -g オプションでシステムにインストール $ npm install -g jasmine-node ちなみに -g オプションなしでインストール  したモジュールのコマンドラインツールは node_modules/.bin/ 以下に入る $ npm install -g jasmine-node $ tree node_modules/.bin/ node_modules/.bin/ $"" jasmine-node -> ../jasmine-node/bin/jasmine-node 0 directories, 1 file 12年12月5日水曜日
  • 72. ソースコードの調整 ブラウザ/Node両方で動作するように修正 // src/Greeter.js function Greeter() {   this.greet = "hello"; } if (typeof exports !== "undefined") {   exports.Greeter = Greeter; } // spec/greeter_spec.js if (typeof exports !== "undefined") {   var Greeter = require("../src/greeter").Greeter; }; describe("Greeter", function() {   it("greetがセットされる", function() {     var greeter = new Greeter();     expect(greeter.greet).toBeDefined();   }); }); 12年12月5日水曜日
  • 73. ソースコードの調整 ブラウザ/Node両方で動作するように修正 // src/Greeter.js function Greeter() {   this.greet = "hello"; } テスト対象コードはQUnitと同じ修正 if (typeof exports !== "undefined") {   exports.Greeter = Greeter; } // spec/greeter_spec.js if (typeof exports !== "undefined") { テスト対象コードのrequire()を追加   var Greeter = require("../src/greeter").Greeter; それ以外はブラウザ実行と同様でOK }; (jasmine-nodeが解決してくれている) describe("Greeter", function() {   it("greetがセットされる", function() {     var greeter = new Greeter();     expect(greeter.greet).toBeDefined();   }); }); 12年12月5日水曜日
  • 74. スペック実行 スペック結果が出力される $ jasmine-node spec . Finished in 0.014 seconds 1 test, 1 assertion, 0 failures jasmine-nodeの引数にはディレクトリを指定 ディレクトリ以下の全スペックファイル (*spec.jsにマッチするファイル)を全て 実行してくれる 12年12月5日水曜日
  • 75. CLI環境での実行 QUnit、Jasmineともにブラウザ上で実行し たソースからテスト(スペック)記述は変更せず に最小限の修正で実行することが可能 しかしまだ、Host ObjectやDOMに依存しな いコードしかCLI環境で実行できない Node上でDOMを実装したモジュールを利用 してCLI環境でテスト可能なコードを増やす 12年12月5日水曜日
  • 76. Node+jsdomを利用した DOM依存コードの実行 12年12月5日水曜日
  • 77. jsdomとは? W3CのDOMをJavaScriptで実装したライブ ラリ(npmモジュール) リモートのHTML/XMLやローカルファイル、 文字列をパースしてDOMオブジェクトを作成 これ使えばWebスクレイピングなど簡単 require("jsdom").env(   "http://www.mapion.co.jp",   ["http://code.jquery.com/jquery.js"],   function (errors, window) {     var alt = window.$("h1 img").attr("alt");     console.log(alt); // 地図検索マップ マピオン   } ); 12年12月5日水曜日
  • 78. npmインストール パッケージ指定してインストール $ npm install jsdom もしくはpackage.json記述してインストール $ cat package.json {   "name": "sample-of-tdd",   "version": "1.0.0",   "devDependencies": {     "qunitjs": "1.10.0",     "qunit-tap": "1.2.2",     "jsdom": "0.2.19"   } } $ npm install 12年12月5日水曜日
  • 79. どう利用するか? jasmine-nodeにはスペック実行ディレクトリ にある「*helpers.js」を読み込んでくれるの で、そこに以下ヘルパー関数を定義 // spec/spec_helpers.js var jsdom = require("jsdom"); global.init_window = function(opt, callback) {   var html = '<html><body></body></html>';   jsdom.env((opt && opt.html) || html, function(errors, window) {     global.window = window;     global.document = window.document;     callback(errors);   }); }; 12年12月5日水曜日
  • 80. ヘルパー関数の利用 beforeEachで初期化処理を走らせれば、初期 化されたwindowとdocumentがグローバルに 作成される // spec/jsdom_spec.js describe("jsdomを利用する", function() {   beforeEach(function(done) {     init_window({       html: '<html><body><div id="hoge">bar</div></body></html>'     }, done);   });   it("documentオブジェクトが利用可能", function() {     expect(document.getElementById("hoge").innerHTML).toEqual("bar");   }); }); 参考:https://github.com/mizchi/sample-node-client-test 12年12月5日水曜日
  • 81. jsdom利用の留意点 windowオブジェクトにはXMLHttpRequest なども定義されており、ほとんどブラウザ しかし、全ての振る舞いが本当のブラウザ上 オブジェクトと同一である保証はない 個人的にはPhantomJSを利用するケースのほ うが多い 12年12月5日水曜日
  • 82. ヘッドレスブラウザ (PhantomJS) 12年12月5日水曜日
  • 83. PhantomJSとは? GUIのない(ヘッドレスな)ブラウザ    JSスクリプトファイルで操作する QtWebKitをベースに作られているため HTML5/CSS3といったモダンブラウザの機 能は実装されている 内部でレンダリングは実行されている    API経由で画面キャプチャも取得できる var page = require("webpage").create(); page.open("http://www.mapion.co.jp/", function(state) {   page.render("mapion.png"); // カレントディレクトリに出力   phantom.exit(); }); 12年12月5日水曜日
  • 84. インストール Windows/MacOSX/Linux向けのバイナリを インストールすれば利用可能 12年12月5日水曜日
  • 85. ユースケース QUnitやJasmineによるテスト実行HTMLの Test Runner Webページのスクリーンキャプチャツール ユーザー操作をエミュレートしたシナリオテ ストの実行 ページリソース(js, css, img)全てを含めたネ ットワークモニタリング 12年12月5日水曜日
  • 86. サンプルコード phantomjsソースツリーに含まれる examples/pizza.js // Find pizza in Mountain View using Yelp var page = require('webpage').create(),     url = 'http://lite.yelp.com/search? find_desc=pizza&find_loc=94040&find_submit=Search'; page.open(url, function (status) {     if (status !== 'success') {         console.log('Unable to access network');     } else {         var results = page.evaluate(function() {             var list = document.querySelectorAll('span.address'), pizza = [], i;             for (i = 0; i < list.length; i++) {                 pizza.push(list[i].innerText);             }             return pizza;         });         console.log(results.join('n'));     }     phantom.exit(); }); 12年12月5日水曜日
  • 87. サンプルコード phantomjsソースツリーに含まれる examples/pizza.js // Find pizza in Mountain View using Yelp var page = require('webpage').create(),     url = 'http://lite.yelp.com/search? find_desc=pizza&find_loc=94040&find_submit=Search'; 単一のページを読み込んでいるブロック page.open(url, function (status) {     if (status !== 'success') {         console.log('Unable to access network');     } else {         var results = page.evaluate(function() {             var list = document.querySelectorAll('span.address'), pizza = [], i;             for (i = 0; i < list.length; i++) {                 pizza.push(list[i].innerText);             }             return pizza;         });         console.log(results.join('n'));     }     phantom.exit(); }); 12年12月5日水曜日
  • 88. サンプルコード phantomjsソースツリーに含まれる examples/pizza.js // Find pizza in Mountain View using Yelp var page = require('webpage').create(),     url = 'http://lite.yelp.com/search? find_desc=pizza&find_loc=94040&find_submit=Search'; ページ内のコンテキストで実行しているブロック page.open(url, function (status) {     if (status !== 'success') { (セキュリティ上の理由で別コンテキスト)         console.log('Unable to access network'); DOMツリーから情報を取得している     } else {         var results = page.evaluate(function() {             var list = document.querySelectorAll('span.address'), pizza = [], i;             for (i = 0; i < list.length; i++) {                 pizza.push(list[i].innerText);             }             return pizza;         });         console.log(results.join('n'));     }     phantom.exit(); }); 12年12月5日水曜日
  • 89. サンプルコード phantomjsソースツリーに含まれる examples/pizza.js // Find pizza in Mountain View using Yelp var page = require('webpage').create(),     url = 'http://lite.yelp.com/search? find_desc=pizza&find_loc=94040&find_submit=Search'; page.open(url, function (status) {     if (status !== 'success') {         console.log('Unable to access network');     } else {         var results = page.evaluate(function() {             var list = document.querySelectorAll('span.address'), pizza = [], i;             for (i = 0; i < list.length; i++) {                 pizza.push(list[i].innerText);             }             return pizza;         });         console.log(results.join('n')); 取得した情報を標準出力して     }     phantom.exit(); ブラウザを終了 }); 12年12月5日水曜日
  • 90. どうTDDで利用するか? Test Runner QUnitやJasmineの実行HTMLを開く テスト実行を待つ 結果HTMLをスクレイピング PhantomJS実行コンテキストで結果出力 GitHubページに各フレームワーク毎に作成さ れているTest Runnerが紹介されている https://github.com/ariya/phantomjs/wiki/Headless-Testing 12年12月5日水曜日
  • 91. PhantomJS QUnit QUnitにbuilt-inされているjsが利用できる $ phantomjs node_modules/qunitjs/addons/phantomjs/runner.js qunit.html Took 22ms to run 20 tests. 20 passed, 0 failed. 12年12月5日水曜日
  • 92. pros/cons pros ブラウザそのもの HTML5/CSS3などモダンな実装が動く WebKitなのでスマートフォンの標準ブラウ ザに近い挙動を期待できる cons WebKit以外のブラウザがターゲットの場 合には積極的に利用できない 12年12月5日水曜日
  • 94. jasmine-gemとは? ブラウザ実行を楽にするヘルパースクリプト (SpecRunner.htmlの作成が不要) コマンドラインからブラウザによるテスト実 行をサポート 仕組みはWebDriver railsコマンドのサポート 12年12月5日水曜日
  • 95. インストール gemでインストール $ gem install jasmine -v 1.3.0 12/3にリリースされたv1.3.1がぶっ壊れて いる?っぽいので v1.3.0 を指定 初期化 rails3プロジェクトの場合 $ rails g jasmine:install railsじゃないプロジェクトの場合 $ jasmine init 12年12月5日水曜日
  • 96. initコマンドの出力 $ jasmine init $ tree . . !"" Rakefile !"" public #   $"" javascripts #   !"" Player.js #   $"" Song.js $"" spec     $"" javascripts         !"" PlayerSpec.js         !"" helpers         #   $"" SpecHelper.js         $"" support             $"" jasmine.yml 6 directories, 6 files 12年12月5日水曜日
  • 97. initコマンドの出力 $ jasmine init $ tree . . !"" Rakefile !"" public #   $"" javascripts #   !"" Player.js #   $"" Song.js standalone版の初期状態と同じ $"" spec サンプルリソース群が出力されている     $"" javascripts         !"" PlayerSpec.js         !"" helpers         #   $"" SpecHelper.js         $"" support             $"" jasmine.yml 6 directories, 6 files 12年12月5日水曜日
  • 98. initコマンドの出力 $ jasmine init $ tree . . Rakefileとjasmine.ymlがstatdalone版に !"" Rakefile 含まれていなかったリソース !"" public #   $"" javascripts #   !"" Player.js #   $"" Song.js $"" spec     $"" javascripts         !"" PlayerSpec.js         !"" helpers         #   $"" SpecHelper.js         $"" support             $"" jasmine.yml 6 directories, 6 files 12年12月5日水曜日
  • 99. Rakeタスクの実行 rake -T コマンドで確認 $ rake -T rake jasmine # Run specs via server rake jasmine:ci # Run continuous integration tests rake jasmineでサーバーが起動、表示された URLにアクセスするとテスト実行できる $ rake jasmine your tests are here:   http://localhost:8888/ ポート指定は環境変数JASMINE_PORT $ JASMINE_PORT=1234 rake jasmine your tests are here:   http://localhost:1234/ 12年12月5日水曜日
  • 100. 読み込むjsの設定 jasmine.ymlで指定可能、ルールや書き方はコ メントに記載されている # src_files # # Return an array of filepaths relative to src_dir to include before jasmine specs. # Default: [] # # EXAMPLE: # # src_files: # - lib/source1.js # - lib/source2.js # - dist/**/*.js # src_files:     - public/javascripts/**/*.js # stylesheets # : (省略) : 12年12月5日水曜日
  • 101. $ rake jasmine:ci WebDriverを利用してブラウザ実行も自動化 $ rake jasmine:ci デフォルトではFirefoxが起動 他のブラウザ起動は環境変数 JASMINE_BROWSERで指定を行う $ JASMINE_BROWSER=chrome rake jasmine:ci 指定可能値 ie, chrome, android, iphone, opera see: https://github.com/vertis/selenium-webdriver/blob/master/lib/selenium/webdriver/common/driver.rb#L25 chrome, android, iphone, operaのdriver 詳細はSeleniumのwikiページにご参照 12年12月5日水曜日
  • 102. 複数ブラウザで実行する 自動テストツール 12年12月5日水曜日
  • 103. 自動テストツール/テストランナー Browser Capturing Unit Testing Automation Browser そもそもユニットテスト用 Selenium ○ - - ではない ユニットテスト + JsTestDriver - ○ ○ ブラウザキャプチャ ユニットテスト + BusterJS - ○ ○ ブラウザキャプチャ Node製 先月(2012/11)公開された Testacular - - ○ ブラウザキャプチャのみ Node製 12年12月5日水曜日
  • 104. ブラウザキャプチャ? サーバーにブラウザを接続させコネクション を維持、サーバー側でコマンドを実行するこ とでテスト実行と結果サマリーを複数ブラウ ザで一気に行う方法 正式名称は知りません 12年12月5日水曜日
  • 105. Testacularを使う 先月末(2012/11)にGoogleからオープンソー ス化して公開されたばかり! Node製でSoket.ioを利用してブラウザキャプ チャを行うシンプルなツール ユニットテストは含まれていない、既存のテ スト資産(Jasmine, Mochaなど)を活用する 12年12月5日水曜日
  • 106. インストール インストールとコマンドラインオプションの 確認 $ npm install -g testacular $ testacular --help Testacular - Spectacular Test Runner for JavaScript. Usage: testacular <command> Commands: start [<configFile>] [<options>] Start the server / do single run. init [<configFile>] Initialize a config file. run [<options>] Trigger a test run. Run --help with particular command to see its description and available options. Options: --help Print usage and options. --version Print current version. 12年12月5日水曜日
  • 107. 前準備 テストリソースをjasmine-gemで用意 $ jasmine init $ tree . . !"" Rakefile !"" public #   $"" javascripts #   !"" Player.js #   $"" Song.js $"" spec     $"" javascripts         !"" PlayerSpec.js         !"" helpers         #   $"" SpecHelper.js         $"" support             $"" jasmine.yml 6 directories, 6 files 12年12月5日水曜日
  • 108. 設定ファイルの作成 initコマンドで対話的に作成してくれる $ testacular init Which testing framework do you want to use ? Press tab to list possible options. Enter to move to the next question. > jasmine Do you want to capture a browser automatically ? Press tab to list possible options. Enter empty string to move to the next question. > Chrome > Firefox > Safari > Which files do you want to test ? You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js". Enter empty string to move to the next question. > public/**/*.js > spec/**/*.js > Any files you want to exclude ? You can use glob patterns, eg. "**/*.swp". Enter empty string to move to the next question. > Do you want Testacular to watch all the files and run the tests on change ? Press tab to list possible options. > yes Config file generated at "/Users/kozy/js-dev/testacular/testacular.conf.js". 12年12月5日水曜日
  • 109. 設定ファイルの作成 initコマンドで対話的に作成してくれる $ testacular init Which testing framework do you want to use ? どのテストフレームワークを利用するか? Press tab to list possible options. Enter to move to the next question. > jasmine デフォルトでJasmineかMochaが選択可 Do you want to capture a browser automatically ? Press tab to list possible options. Enter empty string to move to the next question. > Chrome > Firefox サーバー起動時に接続するブラウザ > Safari 起動後に手動で接続することも可能 > Which files do you want to test ? You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js". Enter empty string to move to the next question. > public/**/*.js テスト実行HTMLに読み込むjsファイル > spec/**/*.js > globパターンで指定可能 Any files you want to exclude ? You can use glob patterns, eg. "**/*.swp". Enter empty string to move to the next question. > 逆に読み込まないjsファイルを指定 Do you want Testacular to watch all the files and run the tests on change ? Press tab to list possible options. > yes ファイル更新を検知して再実行するか Config file generated at "/Users/kozy/js-dev/testacular/testacular.conf.js". 12年12月5日水曜日
  • 110. 設定ファイルを修正 何故かパス設定がうまく動かない... basePathを修正する   1 // Testacular configuration   2 // Generated on Wed Dec 05 2012 23:01:06 GMT+0900 (JST)   3   4   5 // base path, that will be used to resolve files and exclude   6 basePath = '../../../../..';   7   8   9 // list of files / patterns to load in the browser  10 files = [  11 JASMINE,  12 JASMINE_ADAPTER,  13 'public/**/*.js',  14 'spec/**/*.js'  15 ]; : 12年12月5日水曜日
  • 111. 設定ファイルを修正 何故かパス設定がうまく動かない... basePathを修正する   1 // Testacular configuration   2 // Generated on Wed Dec 05 2012 23:01:06 GMT+0900 (JST)   3   4   5 // base path, that will be used to resolve files and exclude 内部ではrequire('path').resolve(basePath, files[i])で   6 basePath = '';   7 解決するため正しいパスが得られない... 空文字列に変更   8   9 // list of files / patterns to load in the browser  10 files = [  11 JASMINE,  12 JASMINE_ADAPTER,  13 'public/**/*.js',  14 'spec/**/*.js'  15 ]; : 12年12月5日水曜日
  • 112. 実行! 以下コマンドでサーバーが起動 $ testacular start 設定ブラウザも起動しキャプチャされる もちろん手動で接続してキャプチャさせる ことも可能(スマホブラウザなど) 読み込みファイルの更新検知、キャプチャ済 みブラウザのリロード、runコマンドの送信で ユニットテストが各ブラウザで自動実行 12年12月5日水曜日
  • 113. 使ってみた感じ 設定ファイルの自動生成など、導入する面倒 くささがまったくない テスト実行がかなり早い、ファイル更新での 自動実行もサクサク動く よくたびたびtestacular経由で起動した Chromeが終了ミス?って親なしプロセスに まだ粗い感じもあるが、かなり使えるツール なのでは? 12年12月5日水曜日
  • 114. CI (Jenkins) 12年12月5日水曜日
  • 116. jarから直接起動 ちゃんと運用する時はTomcatなどアプリケー ションコンテナにデプロイしてください 以下コマンドで8080ポートで起動 $ java -jar jenkins.war 12年12月5日水曜日
  • 117. http://localhost:8080/ 新規ジョブ作成はこっち システムの設定はここ Gitプラグインをまず入れる 12年12月5日水曜日
  • 119. Gitプラグイン取得 チェックを入れて再起動 12年12月5日水曜日
  • 120. 再起動して新規ジョブ作成 フリースタイルでOK 12年12月5日水曜日
  • 121. プロジェクト設定(1) テストなのでローカルパスで 12年12月5日水曜日
  • 122. プロジェクト設定(2) ビルド手順にテスト実行スクリプトを記述 jasmine-nodeのjunitreportはデフォルトで reports以下に結果を出力する 12年12月5日水曜日
  • 123. ビルド実行 手動で実行 12年12月5日水曜日
  • 125. ビルド実行URL 以下URLでビルドが実行される [プロジェクトURL]/build?delay=0sec Gitならコミットフックを仕込むと幸せになれる $ echo 'curl "http://localhost:8080/job/your_project/build?delay=0sec"'>.git/hooks/pre-commit $ chmod +x .git/hooks/pre-commit 12年12月5日水曜日
  • 127. まとめ ブラウザ上で実行するユニットテストツール QUnitとJasmineを紹介しました QUnitとJasmineをベースにTDDで活用でき そうなCLI環境やヘッドレスブラウザの利用方 法を紹介しました ブラウザキャプチャによる複数ブラウザでの ユニットテスト同時実行を紹介しました CIをJenkinsで行うための簡単な設定例を紹介 しました 12年12月5日水曜日
  • 128. 実は... BusterJSを使えば ブラウザ上でユニットテスト出来ます Nodeでユニットテスト出来ます 複数ブラウザの同時実行も出来ます JenkinsなどでCI導入も可能です BusterJSの万能感がハンパないです 12年12月5日水曜日
  • 129. 今後特にウォッチしたい BusterJS Testacular 12年12月5日水曜日
  • 130. 以上 ありがとうございました 12年12月5日水曜日