lxyuma BLOG

開発関係のメモ

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に大体こんな事書いているんじゃない?

  • インスタンス変数の初期化/変更
  • メソッド呼び出し
  • イベントbind/trigger
  • サーバー通信
  • DOM操作(アニメ)
  • window.confirm()等
  • local変数(model等)のCRUD

この中で、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後の検証方法もそれぞれ考えないと行けないし、
    • 問題有ったときの原因の切り分けも面倒くさい。
  • という事で、これもテストダブル必要
  • 色々やり方ある
    1. $.ajaxやsyncにspy/stub
    2. sinon.createFakeServer()(最も良いが設定が面倒臭い)
    3. CRUD各メソッドにstub(instance掴めなければmodel.prototypeにひっかける)

$.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寄り)結合テストに時間かける方がいい気がする

参考