JS開発におけるTDDと自動テストツール利用の勘所 | PPT | Free Download
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日水曜日
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日水曜日
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日水曜日
19. QUnitとは?
ブラウザ上での実行を想定したJSユニットテ
ストフレームワーク
jQueryの開発に利用されている
シンプルさが特徴
MITライセンス
現在のリリースバージョンは v1.10.0
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日水曜日
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日水曜日
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日水曜日
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日水曜日
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日水曜日
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日水曜日
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日水曜日
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日水曜日
130. 以上
ありがとうございました
12年12月5日水曜日