Backbone.jsのテストの悩みとその解決
経緯
Backbone.jsでTDD書く時に、
Model/CollectionはServerSideのようなイメージで書けるのだが、
Viewのテストがfrontend特有の事があり、結構、悩ましい。
ここでは、Backbone.Viewを中心に、
テストで悩ましい所をどう解決していくか、試行錯誤している所等書く。
※どちらかというと、初めにどう書いて行くか?のTDD寄りの話。
ちなみに、環境は、
- Backbone
- Marionette.js
- Test
- mocha
- chai
- sinon.js
を使ってる。
概要
- Backbone.Viewのテストの基本形
- 悩み1: javascript vs html
- 答え:DOM分離
- 悩み2:検証方法が素直に書けない
- 答え:テストダブル
- イベント
- サーバー通信
- アニメ
- local変数
- 答え:テストダブル
- Backboneのテスト現実解
Viewのテストの基本形
初めに、Viewのテストの基本形から。
describe("TodoView", function() { // ①初期化 beforeEach(function() { this.view = new TodoView(); }); // ②メソッド毎にテスト describe(“methods”, function() { it(“should xxxx“, function() { // this.view.yourMethod(); // ③検証 // expect(this.view.ui.title.is(':visible')).to.be.true; }); }); });
こんな感じで書いて行く。
- ①初期化
- view毎回newするの面倒なのでbeforeで。(大体、皆一緒)(initializeの中身をテストするなら、ちょっと調整が必要)
- 場合によってはrender入れてもいいかと。(Marionetteとか。この時、ネスト必要。)
- ②メソッド単位で切り分け
- 関数の実行(eventを経由しない方が良い※後述)
- 検証
- ③検証
- 大抵Viewの$elをchk($(selector)書かない方が良い※後述)
こういう基本的な形を、各環境のgeneratorのはくテンプレートや、
面倒くさければ、editorのsnippetsに登録すると良い。
ここからが、本題。
色んなお悩みを書いて行く。
悩み1
jsだけでなく、html/DOM迄考えなくては行けない事
サーバーサイドのテスト
サーバーサイドのTDDは、以下のたった2つのソースだけを行き来して書けば良かった。
- ターゲットソース
- テストソース
とても、シンプルだし、何よりTDDに集中できる。
実は恵まれていた。
BacboneもModelやCollectionは、同じイメージで書けるのだがViewは少し違う。
Backbone.Viewのテスト
BackboneのViewのテストは、ターゲットとテストソース以外に、
- ターゲットのhtmlや
- 場合に寄ってはテスト用のhtml等のファイルも
行き来が必要になったりする。これらも含めてTDDしようとすると、以下の問題に直面する。
問題1:htmlとターゲットソースだけで完結してしまう問題
実際にhtml上で動かさないと分からない事、調整しないと行けない事も多々あるので、
ファイル横断していく内に、html <=> ターゲットソースの行き来がメインになってしまい、
開発フローの中でのTDDの必然性が薄れてテスト習慣がつかなくなる。
問題2:htmlがデザイン変更等から多くのテスト変更を伴う問題
htmlはデザイン等頻繁に変わる事あるので、
テスト書いてもDOM操作を中心に頻繁に調整が必要になる。
(expext($(‘#aaa’).text()).toBe(‘aaa’)みたいなの一個ずつ直す必要あるし、
test用のでかいtemplateとか準備してるとマジ地獄。
実装直すなら、テストも直すのは当然で正しいのだが、量が多いのが問題。)
問題3:集中力の問題
単純に対象ソースとテストソース見比べれば良かったサーバーサイドのTDDと比べて、
frontendのTDDはあっちこっちファイル見比べてTDD以外の考慮が多いので、
TDDしようとする集中力とやる気を削ぐ。
この集中できないのが一番問題。
jsのTDDに集中するには。
TDD続けるには、とにかく、作業ファイルを
- ターゲットソースと、
- テストソースの
2つだけにするのが良いと思う。
そのために、DOM/htmlを分離する
(TDDする事と、DOM調整する作業を分ける)
DOM/html分離
- DOM
- テストコードにDOMセレクタ書かない
- テストコードからは、Marionette.View.ui経由で掴む
- view.events経由せず、直接関数実行
- Template
- test用fixture template出来るだけ使わない(修正が面倒)
- 最悪、使っても、1,2行の断片程度で
- ターゲットとなるViewも整理する
- 元のViewは、el内だけを扱う。(el以外への変更はtriggerして、別Viewに)
- View内でelを指定しない。相対Viewにする。(これで、テスト用template要らなくなるはず)(Marionetteに沿って書くとRegion使うので、そうなってるはず。)
※ちなみに、勿論、最終的にDOMも貫通したテストは必要で、
それをこのレベルで書くなら、事後的に追加して書いていけば良いし、
書かないなら、別のレベルのテストで確認が必要。
DOM/htmlを完全にスルーするという話ではない。
TDDみたいに、始めてTest書く時にはDOM意識しなくていいのでは?という話。
sample
// View var YourView = Backbone.Marionette.View.extend({ // elは指定しない // ui定義して、DOMアクセスは全てここからにする(変更時、ここを起点に直す) // ※省略してるけどMarionette.Viewならthis.bindUIElements忘れずに ui: { menu: “#menu” }, onClickShowMenuButton: function(){ this.ui.menu.show(); // mocha // test用のfixture template出来るだけ使わない before(function(){ this.view = new YourView()} it(‘should show menu’, function(){ // event経由せず直接メソッド実行 this.view.onClickShowMenuButton(); // ui経由でDOMを掴む expect(this.view.ui.menu.is(‘:visible’)).to.be.true;
悩み2
検証を素直に書けない
Backbone.Viewでちょっとして、html操作であれば、
簡単にテスト書けるのだが、例えばevent挟むと、
何書いていいかわからなくなる。
image的には関数実行後に、素直に
expect(this.view).haveCalledEvent('changeName') // 注意:ただのimageです
と書けたらいいのに。 勿論、書けない。
イベント以外にも、困った事に素直に検証書けない所がいっぱいある
何があるか?整理するためViewに書いてる事を整理すると
Viewに書いてる主要な事
Viewに大体こんな事書いているんじゃない?
この中で、instance変数の変更はサーバーサイドと同じ。悩まない。
メソッド呼び出しも、spy/stubs使えそうなのは、すぐimage湧く通り。
window.confirmも、this.stub(window, "confirm").returns(false);
でいい。
DOM操作は、jQueryでチェックし直せば良い(それぞれのTestFW上でjQuery操作補助するライブラリあるので、それ使うと楽)
この辺りは普通にイメージの湧く通り。
問題は、それ以外の残った所。
残った要素
- イベント
- サーバー通信
- アニメ
- local変数(model等)
これら、全部、色んな事情で結局テストダブルが必要。
イベント
- イベント掴むために、testdouble必要
- event用spyを準備して
- 実行されたかチェックしないといけない
// before this.eventSpy = sinon.spy(); this.view.on("jikken", this.eventSpy); // it this.view.onClickTriggerButton(); expect(this.eventSpy.called).to.be.true;
サーバ通信のfake
- そのまま書くと、
- テストに検証に非同期callback深くなるし、
- POST/PUT後の検証方法もそれぞれ考えないと行けないし、
- 問題有ったときの原因の切り分けも面倒くさい。
- という事で、これもテストダブル必要
- 色々やり方ある
$.ajaxにspy
describe('onClickSave', function(){ beforeEach(function(){ this.ajaxSpy = sinon.spy(Backbone.$, 'ajax'); }); it('should save', function(){ this.view.onClickSaveButton(); expect(this.ajaxSpy.called).to.be.true; }); });
saveにstubs
- stubsのyield(yieldsTo)使うと成功/失敗も掴める
// before this.stub = sinon.stub(this.view.model, "save"); this.stub.yieldsTo('success', {name: "taro"}); // it expect(this.stub.called).to.be.true;
アニメ
- 何秒か待っても書けるが、テスト時間かかる
- sinonのfakeTimer使うとすぐ終わる。
- useFakeTimers()してtick(ミリ秒)するだけ
// before this.fakeTimer = sinon.useFakeTimers(); // it this.view.onClickHideAnime(); this.fakeTimer.tick(2000); expect(this.view.ui.animeButton.is(":visible")).to.be.false;
local変数のチェック
- よく、local変数でYourModelをnewしてsaveしてみたいな事書く
- ここでmockが役立つ!
- モデル.prototypeにmockすると、関数内local変数も検証可
// Backbone側 var Task = Backbone.Model.extend({ url: "/tasks" }); var TestView = Backbone.View.extend({ saveTask: function(){ var task = new Task(); // <--外から掴めない task.save(); }}); // mocha側 // before this.view = new TestView(); this.mock = sinon.mock(Task.prototype); this.expect = this.mock.expects('save'); // it() this.view.saveTask(); expect(this.mock.verify()).to.be.true;
Viewのテストで思った事
これで、一通り、書けるハズ(多分)
しっかし、Backboneのテストはコストかかるなー。
- Backbone.View(特にmarionette)の関数=小粒になる
- 最小単位のUTが効果的と思えない(あまり複雑なlogic無い。関数Aといえば命令Bする!はい終わり!みたいなので、bugが入り込む余地が少ない)
- sinon書くの結構大変。その苦労して書くコストに対して効果が見合わない気がする
- 最小単位のUTより、関数同士の組み合わせやstatus変化の方がbuggy
- initialize => events => 関数Aだけでなく、
- initialize => events => 関数B => status変更 => 関数Aの時どうなる?とか
- 非同期fetchしてる間にタブ切り替えてどうなる?とか
- UTをやらないという話をしているのではないが、UTとしての網羅性に注力するより、ComponentTest/(whitebox寄り)結合テストに注力した方がいいかも
現実解
- Backboneとテスト
- TDDでとりあえずの動作をぱーっと書いて実装する
- その後、UTの網羅性より、ComponentTest/(white寄り)結合テストに時間かける方がいい気がする
参考
- blog
- 書籍
- slide
- source
- BackboneTesting本のcode ※100点とは思わないけど、一番参考になる。