16
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

koaでのテスト

Last updated at Posted at 2014-01-03

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の場合はコールバックが引数に取る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);
  });
});
16
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?