koaでのテスト
ごめんなさい、koajsに浮気しちゃいましたでKoaを紹介しましたが、相変わらずKoaといちゃいちゃしています、今回はKoaで作るアプリケーションのテスティングについてです。
Koaはgeneratorベースでアプリケーションを記述させてくれるフレームワークですが、自分はテスティングも基本的にcoを介してgeneratorベースでテストを書いてみています。co
を利用する際はmocha
実行時に--harmony-generators
オプションを指定する必要があります。
実際のコードは前回と同じく以下において有ります(まだまだ網羅できていないですが...)
まずはExpressからの変更の影響が一番大きいコントローラのテストです。
コントローラのテスト
JSONを返却するだけのAPI用途の場合はsupertestを用いるのが楽です。その場合のテストについてはkoajs/examplesにいっぱいサンプルがあります、例えば https://github.com/koajs/examples/tree/master/hello-world
ここではAPI用途ではなく、テンプレートエンジンを使ってHTMLを描画するようなアプリケーションの場合のコントローラのテストについて紹介します。
Expressの場合はコールバックが引数に取るreq
とres
をモックしてよくテストしてますが、Koaの場合はこのシグネチャが変わってしまっているので違う作戦が必要です。
描画のテスト
Expressではモック版res.render
にスパイを仕掛けて、適切なテンプレートファイルをres.render
に渡しているか検証してましたが、Koaの場合以下のような形で描画を行うので:
exports.index = function *() {
...
this.body = yield render('post/index', { posts: posts.toJSON() });
...
}
モックする余地が残念ながらありません。仕方ないのでrender
の奥で内部的にコールされているテンプレートエンジンの描画メソッドをスタブ(且つスパイ)してテストしてみます、以下jade
の場合:
beforeEach(function() {
// jade.renderFileをスタブ
this.renderFileSpy = sinon.stub(jade, 'renderFile').callsArgWith(2, '');
});
で、実際にコントローラのメソッドをco
の中でコールしてこのスパイが適切にコールされているか検証します:
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へのアクセスは以下のような形で行います:
exports.create = function *() {
...
var data = yield parse(this);
...
};
ここでthis
はKoaではcontextと呼ばれているものでNodeのrequest
とresponse
をくるむオブジェクトです。テストではこのcontext
をモックしてあげる事でデータをコントローラのメソッドに渡します、
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
は例えば以下です:
function stubbedContext() {
this.ctx = _.extend({
req: {
body: {
post: {
title: 'title',
content: 'content'
}
}
},
user: {
id: this.user.id
},
redirect: sinon.spy()
}, this);
}
ビューのテスト
まぁビューはKoaの範疇外ですが、こちらもco
を用いることでテストコードがすっきりします:
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
の招待はこんな感じです:
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つであることを検証しています:
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);
});
});