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