jQuery + Backbone + JasmineでBDD その1

jQuery + Backbone + JasmineでBDDを試してみます。元ネタはこちらのすばらしいブログです。
最初に断っておきますが、私はJasmine初体験でBDD、TDDの知識も本で読みかじった程度です。
ついでに言うと、jQueryもちょろっと触ったことがあるくらいです。

なぜこんな無謀な記事を書いているかといいますと、Jasmineを使ってBDDに関する知識を獲得しつつ、BackboneでMVCフレームワークに触れながら、JavaScriptオブジェクト指向を勉強している間に、jQueryでDOMの操作やセレクターの使い方を覚えられたらサイコーだと思ったからです。わからないことだらけで、いろいろなドキュメントを参照しながら進みます。「それは違う!」とか「こうしたほうがいいんじゃない?」などあれば、ぜひツッコんでいただければと思います。

今回サンプルとして作るのは電話帳アプリです。目標はSpineのサンプルにあるような感じです。
まず最初の一歩は必要なファイルをローカルにダウンロードします。

Jasmine
https://github.com/pivotal/jasmine/downloads

Underscore.js
http://underscorejs.org/

Backbone.js
http://documentcloud.github.com/backbone/

JQuery
GoogleのCDN
https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js

Jasmine standaloneのZipを解凍したら、Underscore.jsとBackbone.jsはlibディレクトリに入れます。
どうやらsrcフォルダにソースを入れて、specフォルダにはSpecを入れる模様です。
準備ができたら次の行をSpecRunner.htmlに追加します。

※読み込む順番を修正しました。最初にjQueryを読み込むようにしないと後でおかしなことになります。

<head>
...
  <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
  <script type="text/javascript" src="lib/underscore.js"></script>
  <script type="text/javascript" src="lib/backbone.js"></script>

ChromeでSpecRunner.htmlを開いてみるとデフォルトのSpec実行結果が表示されています。
ツールからJavaScriptコンソールもついでに開いておきます。

もともとあったspec/*Spec.js、src/*.jsをSpecRunner.htmlのHeadタグから削除して、ContactSpec.js、Contact.jsを作成します。
SpecRunnerのHeadタグ内にそれぞれ追加してRefreshしてみます。まっしろの画面でなにも表示されません。。

とりあえずサンプルのPlay.jsとPlaySpec.jsを参考にしながらSpecを追加してみました。
describeのところで何に関してのテスト(振る舞い?)なのか、二番目の引数にfunction(テストの中身)を渡してます。
contactはこのテストの中で使うモデルで、beforeEachメソッドの中にある内容は、これから追加するSpec(テスト)が実行される直前に、毎回実行されるようです。specが走る前の準備をするためのメソッドということでしょうか?

ContactSpec.js

describe("Contact", function(){
	//Specの中で使うContact
	var contact;
	//各Spec実行前に毎回実行される
	beforeEach(function(){
		contact = new Contact({});	
	});
});

Chromeで画面を更新するとデバッガにUncaught TypeError: Cannot call method 'suiteComplete' of undefinedエラーが表示されました。
どうやらspecがないのでJasmineに怒られている模様です。ContactSpec.jsにSpecを追加してみます。

expectの中には自分が評価したい値を入れて、toEqualにexpect(期待)する結果を入れます。
「expect(contact).toEqual(null)」だと「contactがnullであることをチェックしてね」ということになります。
ここではその逆をやりたいので、Googleで探してみたところ「not」を使うと反対のことをexpectするように指定できるとあったので、やってみました。

	it("is my first spec!", function(){
		expect(contact).not.toEqual(null);
	});

今度はSpecのエラーになってくれました。
Contactがないよ、ということです。

Failing 1 spec
1 spec | 1 failing
Contact is my first spec!.
ReferenceError: Contact is not defined

Contactのコードを追加していないので、Backbone.ModelとしてContact.jsにモデルを定義してみます。

Contact.js

var Contact = Backbone.Model.extend();


Passing 1 Spec
Contact
is my first spec!

初めてPassしました。これがBDD、TDDのRed,Green,Refactorサイクルというやつの入り口でしょうか。
次になにをするのかよくわからないので、getFullNameで氏名が返ってくるようなメソッドを追加してみます。

Backboneのモデルではmodel.set({name: value})がSetterでmodel.get("name")がgetterになっています。
beforeEachでsetを使ってModelにfirstName、lastNameをセットします。

続いて新しいSpecを追加します。
beforeEachでfirstNameとlastNameがセットされているので、getFullNameを呼べば氏名が取得できるはずです。

	beforeEach(function(){
		contact = new Contact();	
		contact.set({firstName: "Taro", lastName: "Yamada"});
	});
	//...略
	it("should generate fullname from first name and lastname", function(){
		expect(contact.getFullName()).toEqual("Yamada Taro");
	});

もちろんモデルになにも書いてないのでRedになりました。

Failing 1 spec
2 specs | 1 failing
Contact should generate fullname from first name and lastname.
TypeError: Object [object Object] has no method 'getFullName'

getFullNameメソッドをモデルに追加してみます。

Contact.js

var Contact = Backbone.Model.extend({
	getFullName: function() {
		return null;
	}
});

なんとなくエラーがマシになりました。

Failing 1 spec
2 specs | 1 failing
Contact should generate fullname from first name and lastname.
Expected null to equal 'Yamada Taro'.

Backbone.Modelのgetを使って取得したfirstName、lastNameを繋げて返します。

	getFullName: function() {
		return this.get("lastName")+" "+this.get("firstName");
	}

全部Greenになりました!
さて、ここからどうしようか…

Passing 2 specs
Contact
is my first spec!
should generate fullname from first name and lastname