koaでのテスト

# koaでのテスト

[ごめんなさい、koajsに浮気しちゃいました](http://qiita.com/p-baleine@github/items/41b8946b9cfff7601d85)で[Koa](http://koajs.com/)を紹介しましたが、相変わらずKoaといちゃいちゃしています、今回はKoaで作るアプリケーションのテスティングについてです。

Koaはgeneratorベースでアプリケーションを記述させてくれるフレームワークですが、自分はテスティングも基本的に[co](https://github.com/visionmedia/co)を介してgeneratorベースでテストを書いてみています。`co`を利用する際は`mocha`実行時に`--harmony-generators`オプションを指定する必要があります。

実際のコードは前回と同じく以下において有ります(まだまだ網羅できていないですが...)

https://github.com/p-baleine/koa-blog-sample

まずはExpressからの変更の影響が一番大きいコントローラのテストです。

## コントローラのテスト

JSONを返却するだけのAPI用途の場合は[supertest](https://github.com/visionmedia/supertest)を用いるのが楽です。その場合のテストについては[koajs/examples](https://github.com/koajs/examples)にいっぱいサンプルがあります、例えば https://github.com/koajs/examples/tree/master/hello-world

ここではAPI用途ではなく、テンプレートエンジンを使ってHTMLを描画するようなアプリケーションの場合のコントローラのテストについて紹介します。

Expressの場合はコールバックが引数に取る`req`と`res`をモックしてよくテストしてますが、Koaの場合はこのシグネチャが変わってしまっているので違う作戦が必要です。

### 描画のテスト

Expressではモック版`res.render`にスパイを仕掛けて、適切なテンプレートファイルを`res.render`に渡しているか検証してましたが、Koaの場合以下のような形で描画を行うので:

```js
exports.index = function *() {
  ...
  this.body = yield render('post/index', { posts: posts.toJSON() });
  ...
}
```

モックする余地が残念ながらありません。仕方ないので`render`の奥で内部的にコールされているテンプレートエンジンの描画メソッドをスタブ(且つスパイ)してテストしてみます、以下`jade`の場合:

```js
beforeEach(function() {
  // jade.renderFileをスタブ
  this.renderFileSpy = sinon.stub(jade, 'renderFile').callsArgWith(2, '');
});
```

で、実際にコントローラのメソッドを`co`の中でコールしてこのスパイが適切にコールされているか検証します:

```js
it('should render `post/index`', function(done) {
  co(function *() {
    yield post.index;
    expect(this.renderFileSpy.args[0][0]).to.match(/post\/index\.jade/);      
  }).call(this, done);
});
```

### POSTとかのテスト

POSTやPUT等bodyを渡すべきテストについて、これはExpressではモックした`req.body`に適当な値を与えてコントローラのメソッドをこれと共にコールして検証していました。`Koa`の場合bodyへのアクセスは以下のような形で行います:

```js
exports.create = function *() {
  ...
  var data = yield parse(this);
  ...
};
```

ここで`this`はKoaでは[context](https://github.com/koajs/koa/blob/master/docs/api/context.md)と呼ばれているものでNodeの`request`と`response`をくるむオブジェクトです。テストではこの`context`をモックしてあげる事でデータをコントローラのメソッドに渡します、

```js
describt('POST /', function() {
  beforeEach(function() {
    stubbedContext.call(this); // this.ctxにモック版のcontextを割り当て
  });

  it('should save post with user_id', function(done) {
    co(function *() {
      yield post.create; // `post.create`の中で`this`は`this.ctx`
      ...
    }).call(this.ctx, done);
  });
})
```

`stubbedContext`は例えば以下です:

```js
function stubbedContext() {
  this.ctx = _.extend({
    req: {
      body: {
        post: {
          title: 'title',
          content: 'content'
        }
      }
    },
    user: {
      id: this.user.id
    },
    redirect: sinon.spy()
  }, this);
}
```

## ビューのテスト

まぁビューはKoaの範疇外ですが、こちらも`co`を用いることでテストコードがすっきりします:

```js
describe('post/index view', function() {
  function *renderIndex(posts) {
    var viewFile = join(__dirname, '../../../lib/views/post/index.jade');
    return (yield render(viewFile, {
      formatDate: function() {},
      posts: posts
    }));
  }

  it('should render posts', function(done) {
    co(function *() {
      var window = yield renderIndex(posts);
      expect(window.$('.post')).to.have.length(posts.length);
    })(done);
  });
});
```

`renderIndex`の中で呼んでいる`render`の招待はこんな感じです:

```js
var coRender = require('co-render');
var jsdom = require('jsdom');
var join = require('path').join;
var fs = require('fs');
var option = { encoding: 'utf-8' };
var jquery = fs.readFileSync(join(__dirname, '/../../bower_components/jquery/jquery.min.js'), option);

function *render(view, opts) {
  var body = yield coRender(view, opts);

  return (yield function(cb) {
    jsdom.env({
      html: body,
      src: [jquery],
      done: cb
    });
  });
}

module.exports = render;
```

引数にもらったviewのパスのファイルを`co-render`で描画して得られるHTML文字列を`jsdom`でパースします、この際に`jquery`も一緒に仕込んでおいてテストでの検証をやりやすくしています。

## モデルのテスト

モデルもKoaには関係ない話です、やはり`co`を用いることで非同期処理のテストが容易になります。以下は`Bookshelf`で実装したブログの投稿モデルの`findAll`メソッドのテストです、`Post`モデルを2つ`save`しておいて、`findAll`した際に得られる`Post`モデルが2つであることを検証しています:

```js
describe('.findAll()', function() {
  beforeEach(function(done) {
    co(function *() {
      var user = yield new User({ email: 'p.baleine@gmail.com', name: 'p', password: 's' }).save();

      yield new Post({ title: 'title1', content: 'content1', user_id: user.id }).save();
      yield new Post({ title: 'title2', content: 'content2', user_id: user.id }).save();
    })(done);
  });

  it('should return all posts.', function(done) {
    co(function *() {
      var posts = yield Post.findAll();

      expect(posts).to.have.length(2);
    })(done);
  });
});
```