SestaでMVCアプリのテストを書く

[Testing Sencha Apps by Mats Bryntse (Sencha Touch North West Meetup)](http://www.youtube.com/watch?v=nhokEFzzjcI&feature=youtu.be&hd=1)
という動画を見てSiestaに興味を持ったので、さわり始めてみました。

![](http://extjs.sunvisor.net/wp-content/uploads/2012/11/siesta.png)

この画面は、この記事の最後のテスト、GridPanelのテストを実行したときの画面です。画面上でテストを選択して実行させる。ビジュアルコンポーネントのテストでは、その表示も確認できます。冒頭のビデオを見ていても、とてもワクワクするような作りになっています。

### Siestaの配置

Siestaのexampleに従って次のようにしました。

      ▾ public_html/
        ▾ ex_siesta/
          ▸ app/
          ▸ data/
          ▸ resources/
          ▸ siesta/   <-- SiestaのSDK
          ▸ tests/
            test_harness.html
            test_harness.js
            app.js
            index.html
        ▸ extjs/        <-- Ext JS のSDK

テストの起点となるのは、test_harness.html と test_harness.js です。

**test_harness.html**

    <!DOCTYPE html>
    <html>
        <head>
            <link rel="stylesheet" type="text/css" href="../../extjs/resources/css/ext-all.css"/>
            <link rel="stylesheet" type="text/css" href="siesta/resources/css/siesta-all.css">
            
            <script type="text/javascript" src="../../extjs/ext-all.js"></script>
            <script type="text/javascript" src="siesta/siesta-all.js"></script>

            <script type="text/javascript" src="test_harness.js"></script>
        </head>
        
        <body>
        </body>
    </html>

CSSとJS、それぞれExt JSとSiestaのものを読み込みます。
最後に、`test_harness.js`を読み込みます。

### Harness

**test_harness.js**

    var Harness = Siesta.Harness.Browser.ExtJS;

    Harness.configure({
        title       : 'MVC Test Suite',
        loaderPath  : { 'EX' : 'app' },
        
        preload : [
           &quot;../extjs/resources/css/ext-all.css&quot;,
           &quot;../extjs/ext-all-debug.js&quot;
        ]
        
    });

    Harness.start({
        group: 'Sanity',
        items: [
            'tests/010_sanity.t.js'
        ]
    },{
        group: 'Model',
        items: [
            'tests/021_employee.t.js',
            'tests/022_depertment.t.js'
        ]
    },{
        group: 'View',
        items: [
            'tests/031_list.t.js',
            'tests/032_grid.t.js'
        ]
    });

Harnessの設定

+ **`loaderPath`**
  loaderPathには、アプリケーションの名前空間へのパスをセットします。
  (Ext.loaderに設定するのと同じですね)
+ **`preload`**  
  preloadには各テストをロードする前にロードすべきファイルを指定します。

## テストコード

実際のテストは、この場合testsディレクトリの下に置きます。
それぞれのテストは、完全に独立して実行されグローバルスコープもクリーンナップされます。

**tests/010_sanity.t.js**

    StartTest(function(t) {
        t.diag(&quot;Sanity test, loading classes on demand and verifying they were indeed loaded.&quot;);
        
        t.ok(Ext, 'ExtJS is here');
        t.requireOk('EX.controller.Main');
    });

StartTestという関数の引数に無名関数を渡しています。

テストコードは
`StartTest(function (t) { .... })`
でラップします。
StartTestに渡す関数に第1引数として渡される引数(前記の`t`)は、Siesta.Testのインスタンスです。
で、Siesta.Test のインスタンスにはいくつかのアサーションメソッドがあります。
それを使ってテストを書いて行きます。ここで使っているのは次のようなものです。

+ **`ok`メソッド**  
  値が真になる場合にテストをパスします
+ **`requireOk`メソッド**  
  Siesta.Test.ExtJSクラスのメソッド。Ext.require()で指定したクラスをロードして、コールバック関数が指定されていたら、それを実行する。
+ **`done`メソッド**  
  ここでは使ってませんが、テストを終了するメソッドです。

### Modelのテスト

    StartTest(function(t) {

        t.requireOk('EX.model.Employee', function() {
            var mod = Ext.create('EX.model.Employee', {
                name: '鬼瓦　権三',
                department_id: 1,
                email: 'gonzo@onigawara.com',
                gender: '男',
                age: 33
            });
            
            t.is(mod.genderEn(), 'male', 'genderEn works ok');
            t.is(mod.get('email'), 'gonzo@onigawara.com', 'Could read email');
        });
    });

+ **`is`メソッド**  
  1つ目と2つ目の引数が等しければテストをパスします

genderEnというメソッドは、Modelで定義しているメソッドで

    genderEn: function(value) {
        var me = this,
            gender = me.get('gender');

        if(gender == '男'){
            return 'male';
        } else if(gender == '女'){
            return 'female';
        } else {
            return gender;
        }
    }

こんな感じのメソッドです。このテストでは、isメソッドでその動作を確認しているのと、
普通にgetメソッドでemailを読み出して、値を比較してみています。

### Storeのテスト

    StartTest(function(t) {
        t.requireOk('EX.store.Departments', function() {
            var s = Ext.create('EX.store.Departments'),
                async = t.beginAsync();

            s.load({
                callback: function() {
                    var c, m;

                    t.pass('loaded');
                    c = s.getCount();
                    t.isGreater(c, 0, Ext.String.format('レコードが{0}件', c));
                    m = s.getAt(0);
                    t.ok(m, Ext.String.format('最初のレコードのname:{0}', m.get('name')));
                    t.endAsync(async);
                }
            });
            
        });
    });

ストアのテストでは、ストアをloadしたコールバックでテストを実行しました。

非同期のコールバックを使ったテストをする場合は、beginAsync/ednAsyncを使って、その非同期呼び出しが完了するまで、
テストが終了しないようにする必要があります。

### View のテスト

Gridをテストしてみます。

    StartTest(function(t) {
        t.requireOk(
            [
                'EX.model.Employee', 
                'EX.store.Employees', 
                'EX.model.Department', 
                'EX.store.Departments', 
                'EX.view.Grid'
            ], 
            function() {
                var grid;
                
                Ext.create('EX.store.Employees',{
                    storeId: 'Employees'
                });
                Ext.create('EX.store.Departments', {
                    storeId: 'Departments'
                });

                grid = Ext.create('EX.view.Grid',{
                    renderTo: Ext.getBody()
                });

                t.waitForRowsVisible(grid, function() {
                    t.is(grid.store.getCount(), grid.getView().getNodes().length, 'Rendered all data in store ok');
                    t.matchGridCellContent(grid, 0, 1, grid.store.first().get('name'), 'Found full name in first cell');
                });
            }
        );
    });

テストできるようになるまで、ちょっとハマりました。
上記で、ストアを生成する時に、storeIdを指定しています。テスト対象のストアにはstoreIdの定義はありません。
通常、Gridではstoreコンフィグに文字列でストアを指定しますね。

    Ext.define('EX.view.Grid', {
        store: 'Users'
        :
        :
    });

こんな感じですよね。ここで指定している 'Users' っていうのは、ストアのstoreIdになります。
MVCではstoreIdをつけていなくても、クラス名からstoreIdを生成してストアをStoreManagerに登録してくれますが、
テストの状態ではそんなことはありません。
ですので、ストアをインスタンス化するときにstoreIdを指定してやる必要があります。

で、グリッドを生成してやると、Siestaの画面にちゃんとGridが表示されます。
t.waitForRowsVisible を使ったテストは、Siestaのサンプルの中にあったものです。
こういったExt JSのテストに特化したメソッドが用意されているのはいいですね。