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

33,557 views

Published on

Published in: Technology
2 Comments
172 Likes
Statistics
Notes
No Downloads
Views
Total views
33,557
On SlideShare
0
From Embeds
0
Number of Embeds
2,071
Actions
Shares
0
Downloads
0
Comments
2
Likes
172
Embeds 0
No embeds

No notes for slide

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

  1. 1. JS開発における TDDと自動テスト ツール利用の勘所 2012.12.06 株式会社マピオン 中村 浩士12年12月5日水曜日
  2. 2. 自己紹介 中村 浩士 ( @kozy4324 ) 株式会社マピオン所属 主にWebアプリのフロントエンド開発 JavaScript, ActionScript12年12月5日水曜日
  3. 3. Mapion12年12月5日水曜日
  4. 4. Mapion 地図情報検索サイト 月間7600万PV、1200万UU 全文検索エンジンSolrを利用した900万件超 のスポット情報を検索できる電話帳/地図面 その他位置情報コンテンツやナビサービス 2008年からスマートフォンサイトにも注力 Android、iOSのネイティブアプリ開発も12年12月5日水曜日
  5. 5. 地図を使ったWebアプリ よく開発しています12年12月5日水曜日
  6. 6. 今回 話すること TDDの基本となるJSユニットテストツールの 使い方 WebアプリでのTDDを意識した設計について (少しだけ) 様々なツールを利用してTDD/自動テストの効 率化を試みる話12年12月5日水曜日
  7. 7. 話しないこと TDD自体について 詳細なやり方、あるべき論 WebアプリでのTDDベストプラクティス 僕はまだその答えに辿り着いていないです... 「テスト駆動JavaScript」が良書なので、それを読みましょう Webアプリに対するシナリオベースの     自動テストについて ユーザー操作をエミュレートしてWebアプ リ全体の振る舞いを自動でテストする方法12年12月5日水曜日
  8. 8. アジェンダ ブラウザ上で実行するJSユニットテストツール 各ツール比較 使い方&コードサンプル WebアプリのTDDを意識した設計について TDDや自動テストで活用できる各種ツール コマンドライン環境 ヘッドレスブラウザ 自動テストツール CI環境12年12月5日水曜日
  9. 9. ブラウザ上で実行する JSユニットテストツール12年12月5日水曜日
  10. 10. TDDとは Test-Driven Development(テスト駆動開発) 分析技法、設計技法( テスト技法) 正しく動くソフトウェアを確実に作り上げるため のテクニック 進め方 1. テストを書く(テストファースト) 2. テストをパスする最低限の実装を行う 3. テストのパスを保持したままコードの重複を除 去する(リファクタリング) 4. 1∼3を短いスパンで繰り返す12年12月5日水曜日
  11. 11. TDDの効果 書いたプログラムに対する即座のフィードバック 要求の理解の促進 リファクタリングの支援、クリーンコードの促進 自動テストによるデグレード検知 プログラマが持つ不安の解消 心の健康をもたらす :)12年12月5日水曜日
  12. 12. JSユニットテストツール JsUnit YUI Test Google Closure Tools QUnit Jasmine Mocha Vows (etc...)12年12月5日水曜日
  13. 13. JSユニットテストツール JsUnit YUI Test Google Closure Tools QUnit 自分がよく利用するのはこの4つ QUnit, Jasmine, Mochaはブラウザ上で実行可能 Jasmine Mocha Vows (etc...)12年12月5日水曜日
  14. 14. ざっくり比較 非同期 スタイル ブラウザ実行 CLI実行 サポート シンプル QUnit フラット ○ △ ○ ブラウザ実行に最適 Rubyist向け Jasmine BDD ○ ○ ○ Jasmine-gemくそ便利 BDD, TDD, Exports, Nodeモジュール Mocha フラットが選べる ○ ○ ○ フレキシブル Nodeモジュール Vows Exports ○ ○ Nodeの非同期処理テストが スマートに書ける12年12月5日水曜日
  15. 15. どれを利用すればよい?12年12月5日水曜日
  16. 16. ケース別 プロジェクトへの導入が目的 シンプルなQUnitがオススメ Ruby / Ruby on Railsがメインの領域な人 Jasmineがオススメ CLI得意 / Node.jsもやりたい! Mocha, Vowsがいいのでは?12年12月5日水曜日
  17. 17. QUnitとJasmineの 使い方を紹介12年12月5日水曜日
  18. 18. QUnit12年12月5日水曜日
  19. 19. QUnitとは? ブラウザ上での実行を想定したJSユニットテ ストフレームワーク jQueryの開発に利用されている シンプルさが特徴 MITライセンス 現在のリリースバージョンは v1.10.012年12月5日水曜日
  20. 20. http://qunitjs.com サイトからjsとcssをダウンロードして利用可能12年12月5日水曜日
  21. 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 install12年12月5日水曜日
  22. 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. 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. 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. 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. 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. 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日水曜日
  28. 28. 実行結果12年12月5日水曜日
  29. 29. 実行結果 onでグローバルへの変数汚染を チェックするモードで再実行 モジュールでの絞り込み実行も可能 リストをクリックすると詳細を開閉 (エラー時は最初から開いている) Rerun選択 or ダブルクリックで 特定テストのみ再実行 再実行時はfailしたテストから実行する(sessionStorage利用してる) その仕様を知らずに順番に依存したテストを書くと死ねます...12年12月5日水曜日
  30. 30. failした時はこんな感じ12年12月5日水曜日
  31. 31. Jasmine12年12月5日水曜日
  32. 32. Jasmineとは? RubyのRSpecライクな記法のBDD(ビヘイ ビア駆動開発)フレームワーク 豊富なExpectationsとMatchers (QUnitで言うアサーション) spyによるTest Double(テスト代役) プラガブルなReporter MITライセンス 現在のリリースバージョンは v1.3.012年12月5日水曜日
  33. 33. https://github.com/pivotal/jasmine GitHubプロジェクトページのDownloadsにある zipファイルをダウンロードして解凍12年12月5日水曜日
  34. 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. 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 files12年12月5日水曜日
  36. 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. 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 → expectations12年12月5日水曜日
  38. 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. 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 → 親のafterEach12年12月5日水曜日
  40. 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. 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日水曜日
  42. 42. 実行結果12年12月5日水曜日
  43. 43. 実行結果 spec毎のpass or failの結果 specのテキストをクリックすると、 該当スペックのみを再実行12年12月5日水曜日
  44. 44. failした時はこんな感じ12年12月5日水曜日
  45. 45. QUnit vs Jasmine12年12月5日水曜日
  46. 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. 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. 48. QUnit vs Jasmine ブラウザ上の実行では基本機能は同等 記述スタイルの違い、好みの問題 QUnitはボキャブラリーが絞られるので  簡潔にならざるを得ない、表現力は劣る Jasmineは構造化しやすいがネストが深く なりがち(平均3∼5)、記述量も多め12年12月5日水曜日
  49. 49. WebアプリのTDDを 意識した設計について12年12月5日水曜日
  50. 50. TDDやりづらい実装 host objectに強依存 host objectとは実行環境から提供されるオ ブジェクト ex) window, navigator, location, etc... DOMオブジェクトに強依存12年12月5日水曜日
  51. 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. 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. 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. 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. 55. host object/DOMへの 依存を分離した設計で Nodeなどの別環境でも ユニットテストが書ける12年12月5日水曜日
  56. 56. TDDや自動テストで 活用できる各種ツール12年12月5日水曜日
  57. 57. コマンドライン環境 (CLI)12年12月5日水曜日
  58. 58. CLIでTDDする動機 ブラウザ実行での コード修正→保存→アプリ ケーション切替→ブラウザ再読み込み、この 手順が煩雑 ブラウザ実行ではテスト全体の実行と結果確 認が自動化されていない つまり、このままではJenkinsなどのCI環 境に組み込みづらい12年12月5日水曜日
  59. 59. CLIを持つ主なJS処理系 SpiderMonkey C言語実装、Mozillaで保守 Rhino Java実装、Mozillaで保守 JDK6以降にbundleされている Node.js サーバーサイドJS実行環境 処理系はChromeと同じV8エンジン 同梱されるパッケージ管理のnpmが便利12年12月5日水曜日
  60. 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. 61. Node.jsのインストール 各プラットフォーム向けのインストーラーを取得 (ただしCygwinは5.10でサポート外...)12年12月5日水曜日
  62. 62. QUnitをNodeで動かす12年12月5日水曜日
  63. 63. QUnit + QUnit-TAP QUnit自体に標準出力へテスト結果をレポー トする機能がない npmモジュールとして公開されている  QUnit-TAPを組み合わせるのがオススメ12年12月5日水曜日
  64. 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 install12年12月5日水曜日
  65. 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. 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. 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. 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: PASS12年12月5日水曜日
  69. 69. JasmineをNodeで動かす12年12月5日水曜日
  70. 70. Jasmine-node Jamine-coreとそれを実行するCLIで構成され るnpmモジュール オプションでJUnit XMLフォーマットで出力 などCLI向けの拡張がいくつかなされている12年12月5日水曜日
  71. 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 file12年12月5日水曜日
  72. 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. 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. 74. スペック実行 スペック結果が出力される $ jasmine-node spec . Finished in 0.014 seconds 1 test, 1 assertion, 0 failures jasmine-nodeの引数にはディレクトリを指定 ディレクトリ以下の全スペックファイル (*spec.jsにマッチするファイル)を全て 実行してくれる12年12月5日水曜日
  75. 75. CLI環境での実行 QUnit、Jasmineともにブラウザ上で実行し たソースからテスト(スペック)記述は変更せず に最小限の修正で実行することが可能 しかしまだ、Host ObjectやDOMに依存しな いコードしかCLI環境で実行できない Node上でDOMを実装したモジュールを利用 してCLI環境でテスト可能なコードを増やす12年12月5日水曜日
  76. 76. Node+jsdomを利用した DOM依存コードの実行12年12月5日水曜日
  77. 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. 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 install12年12月5日水曜日
  79. 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. 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-test12年12月5日水曜日
  81. 81. jsdom利用の留意点 windowオブジェクトにはXMLHttpRequest なども定義されており、ほとんどブラウザ しかし、全ての振る舞いが本当のブラウザ上 オブジェクトと同一である保証はない 個人的にはPhantomJSを利用するケースのほ うが多い12年12月5日水曜日
  82. 82. ヘッドレスブラウザ (PhantomJS)12年12月5日水曜日
  83. 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. 84. インストール Windows/MacOSX/Linux向けのバイナリを インストールすれば利用可能12年12月5日水曜日
  85. 85. ユースケース QUnitやJasmineによるテスト実行HTMLの Test Runner Webページのスクリーンキャプチャツール ユーザー操作をエミュレートしたシナリオテ ストの実行 ページリソース(js, css, img)全てを含めたネ ットワークモニタリング12年12月5日水曜日
  86. 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. 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. 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. 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. 90. どうTDDで利用するか? Test Runner QUnitやJasmineの実行HTMLを開く テスト実行を待つ 結果HTMLをスクレイピング PhantomJS実行コンテキストで結果出力 GitHubページに各フレームワーク毎に作成さ れているTest Runnerが紹介されている https://github.com/ariya/phantomjs/wiki/Headless-Testing12年12月5日水曜日
  91. 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. 92. pros/cons pros ブラウザそのもの HTML5/CSS3などモダンな実装が動く WebKitなのでスマートフォンの標準ブラウ ザに近い挙動を期待できる cons WebKit以外のブラウザがターゲットの場 合には積極的に利用できない12年12月5日水曜日
  93. 93. jasmine-gem12年12月5日水曜日
  94. 94. jasmine-gemとは? ブラウザ実行を楽にするヘルパースクリプト (SpecRunner.htmlの作成が不要) コマンドラインからブラウザによるテスト実 行をサポート 仕組みはWebDriver railsコマンドのサポート12年12月5日水曜日
  95. 95. インストール gemでインストール $ gem install jasmine -v 1.3.0 12/3にリリースされたv1.3.1がぶっ壊れて いる?っぽいので v1.3.0 を指定 初期化 rails3プロジェクトの場合 $ rails g jasmine:install railsじゃないプロジェクトの場合 $ jasmine init12年12月5日水曜日
  96. 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 files12年12月5日水曜日
  97. 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 files12年12月5日水曜日
  98. 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 files12年12月5日水曜日
  99. 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. 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. 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. 102. 複数ブラウザで実行する 自動テストツール12年12月5日水曜日
  103. 103. 自動テストツール/テストランナー Browser Capturing Unit Testing Automation Browser そもそもユニットテスト用 Selenium ○ - - ではない ユニットテスト + JsTestDriver - ○ ○ ブラウザキャプチャ ユニットテスト + BusterJS - ○ ○ ブラウザキャプチャ Node製 先月(2012/11)公開された Testacular - - ○ ブラウザキャプチャのみ Node製12年12月5日水曜日
  104. 104. ブラウザキャプチャ? サーバーにブラウザを接続させコネクション を維持、サーバー側でコマンドを実行するこ とでテスト実行と結果サマリーを複数ブラウ ザで一気に行う方法 正式名称は知りません12年12月5日水曜日
  105. 105. Testacularを使う 先月末(2012/11)にGoogleからオープンソー ス化して公開されたばかり! Node製でSoket.ioを利用してブラウザキャプ チャを行うシンプルなツール ユニットテストは含まれていない、既存のテ スト資産(Jasmine, Mochaなど)を活用する12年12月5日水曜日
  106. 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. 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 files12年12月5日水曜日
  108. 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. 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. 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. 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. 112. 実行! 以下コマンドでサーバーが起動 $ testacular start 設定ブラウザも起動しキャプチャされる もちろん手動で接続してキャプチャさせる ことも可能(スマホブラウザなど) 読み込みファイルの更新検知、キャプチャ済 みブラウザのリロード、runコマンドの送信で ユニットテストが各ブラウザで自動実行12年12月5日水曜日
  113. 113. 使ってみた感じ 設定ファイルの自動生成など、導入する面倒 くささがまったくない テスト実行がかなり早い、ファイル更新での 自動実行もサクサク動く よくたびたびtestacular経由で起動した Chromeが終了ミス?って親なしプロセスに まだ粗い感じもあるが、かなり使えるツール なのでは?12年12月5日水曜日
  114. 114. CI (Jenkins)12年12月5日水曜日
  115. 115. jarのダウンロード12年12月5日水曜日
  116. 116. jarから直接起動 ちゃんと運用する時はTomcatなどアプリケー ションコンテナにデプロイしてください 以下コマンドで8080ポートで起動 $ java -jar jenkins.war12年12月5日水曜日
  117. 117. http://localhost:8080/ 新規ジョブ作成はこっち システムの設定はここ Gitプラグインをまず入れる12年12月5日水曜日
  118. 118. Gitプラグイン取得12年12月5日水曜日
  119. 119. Gitプラグイン取得 チェックを入れて再起動12年12月5日水曜日
  120. 120. 再起動して新規ジョブ作成 フリースタイルでOK12年12月5日水曜日
  121. 121. プロジェクト設定(1) テストなのでローカルパスで12年12月5日水曜日
  122. 122. プロジェクト設定(2) ビルド手順にテスト実行スクリプトを記述 jasmine-nodeのjunitreportはデフォルトで reports以下に結果を出力する12年12月5日水曜日
  123. 123. ビルド実行 手動で実行12年12月5日水曜日
  124. 124. 結果が確認できる12年12月5日水曜日
  125. 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-commit12年12月5日水曜日
  126. 126. まとめ12年12月5日水曜日
  127. 127. まとめ ブラウザ上で実行するユニットテストツール QUnitとJasmineを紹介しました QUnitとJasmineをベースにTDDで活用でき そうなCLI環境やヘッドレスブラウザの利用方 法を紹介しました ブラウザキャプチャによる複数ブラウザでの ユニットテスト同時実行を紹介しました CIをJenkinsで行うための簡単な設定例を紹介 しました12年12月5日水曜日
  128. 128. 実は... BusterJSを使えば ブラウザ上でユニットテスト出来ます Nodeでユニットテスト出来ます 複数ブラウザの同時実行も出来ます JenkinsなどでCI導入も可能です BusterJSの万能感がハンパないです12年12月5日水曜日
  129. 129. 今後特にウォッチしたい BusterJS Testacular12年12月5日水曜日
  130. 130. 以上 ありがとうございました12年12月5日水曜日

×