• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
JS開発におけるTDDと自動テストツール利用の勘所
 

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

on

  • 28,651 views

 

Statistics

Views

Total Views
28,651
Views on SlideShare
26,790
Embed Views
1,861

Actions

Likes
160
Downloads
0
Comments
2

28 Embeds 1,861

http://futoase.github.io 424
http://labs.mapion.co.jp 332
http://futoase.github.com 274
http://makoto-tanaka.com 231
https://twitter.com 166
http://weed.cocolog-nifty.com 118
http://kozy4324.github.io 100
http://weed.hatenablog.com 55
http://localhost 50
http://jaundiced7.putangas.com 23
http://s.deeeki.com 20
http://kozy4324.github.com 17
http://vm-github01.vdev.mapion.co.jp 12
http://app.m-cocolog.jp 7
https://twimg0-a.akamaihd.net 7
http://www.techgig.com 5
http://webcache.googleusercontent.com 4
http://p1178.office.mapion.co.jp 4
http://cloud.feedly.com 2
http://b.hatena.ne.jp 2
https://web.tweetdeck.com 1
https://www.chatwork.com 1
http://tweetedtimes.com 1
http://favtile.com 1
http://app.cocolog-nifty.com 1
https://si0.twimg.com 1
http://twitter.com 1
http://jaundiced7.rssing.com 1
More...

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

12 of 2 previous next

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

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

    • JS開発における TDDと自動テスト ツール利用の勘所 2012.12.06 株式会社マピオン 中村 浩士12年12月5日水曜日
    • 自己紹介 中村 浩士 ( @kozy4324 ) 株式会社マピオン所属 主にWebアプリのフロントエンド開発 JavaScript, ActionScript12年12月5日水曜日
    • Mapion12年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日水曜日
    • QUnit12年12月5日水曜日
    • QUnitとは? ブラウザ上での実行を想定したJSユニットテ ストフレームワーク jQueryの開発に利用されている シンプルさが特徴 MITライセンス 現在のリリースバージョンは v1.10.012年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 install12年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日水曜日
    • Jasmine12年12月5日水曜日
    • Jasmineとは? RubyのRSpecライクな記法のBDD(ビヘイ ビア駆動開発)フレームワーク 豊富なExpectationsとMatchers (QUnitで言うアサーション) spyによるTest Double(テスト代役) プラガブルなReporter MITライセンス 現在のリリースバージョンは v1.3.012年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 files12年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 → expectations12年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 → 親のafterEach12年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 Jasmine12年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 install12年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: PASS12年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 file12年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 install12年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-test12年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-Testing12年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-gem12年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 init12年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 files12年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 files12年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 files12年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 files12年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.war12年12月5日水曜日
    • http://localhost:8080/ 新規ジョブ作成はこっち システムの設定はここ Gitプラグインをまず入れる12年12月5日水曜日
    • Gitプラグイン取得12年12月5日水曜日
    • Gitプラグイン取得 チェックを入れて再起動12年12月5日水曜日
    • 再起動して新規ジョブ作成 フリースタイルでOK12年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-commit12年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 Testacular12年12月5日水曜日
    • 以上 ありがとうございました12年12月5日水曜日