koaでのテスト

  • 16
    Like
  • 0
    Comment
More than 1 year has passed since last update.

koaでのテスト

ごめんなさい、koajsに浮気しちゃいましたKoaを紹介しましたが、相変わらずKoaといちゃいちゃしています、今回はKoaで作るアプリケーションのテスティングについてです。

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

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

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

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

コントローラのテスト

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

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

Expressの場合はコールバックが引数に取るreqresをモックしてよくテストしてますが、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のrequestresponseをくるむオブジェクトです。テストではこの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);
  });
});