業務的な何かやら、ハッカソンなどで使ったので感想

http://sailsjs.org/

名前の通り、Ruby on Rails っぽさがあるフレームワークで、
APIづくりなどで力をいかんなく発揮しそう。

良い所

テストについては、mocha と supertest 、あと barrels を組み合わせて行っています。

supertest は、テスト用にHTTPリクエストのために使って、barrels  は、データベースの初期データなどを用意するために使ってます。以下のような感じでテストが書けてます。

(ココらへんとか参考にしてます。 https://groups.google.com/forum/#!topic/sailsjs/yD6l9ii7R1g)

test/global.test.js

var Sails = require('sails')
  , barrels = require('barrels');

before(function (done) {
  Sails.lift({
    log: {
      level: 'error',

    },
    adapters: {
      // データベースはメモリを使う(テスト終了後に消える)
      default: 'sails-memory'
    },
    // port は 9999 を利用
    port: 9999
  }, function (err, sails) {
    if (err) return done(err);

    this.app = sails;
    // これで test/fixtures の下にある json を元にテストデータが作られる
    // https://github.com/bredikhin/barrels 参照
    this.fixtures = barrels.objects;
    barrels.populate(function (err) {
      done(err);
    });
  });
});

after(function (done) {
  sails.lower(done);
});

test/FooController.test.js

describe('Foo API', function () {
  describe('#find()', function () {
    it('should return ok', function (done) {
      request(app.express.app)
        .get('/foo')
        .set('Accept', 'application/json')
        .expect('Content-Type', /json/)
        .expect(200)
        .end(function (err, res) {
          assert.equal(err, null);
          assert.equal(res.body.result, 'ok');

          done();
        });
    });
});

うーん。ってところ。

  • 0.9時点の話だが、express.js との連携がべったりしてしまっているため、例えばこの処理を取り除きたいというのができなかったりする。0.10だとその辺の処理がざっと変わるらしい。期待したい。
  • ドキュメントに書いていない隠し仕様的なものが、よくあったりするので、結局 sails.js のソースコードを探求することになる。セッションを無効化する場合は、Session 設定の adapter を null にすればよいのだが、この点はドキュメントに一切記されていない。http://sailsjs.org/#!documentation/config.session コントリビュートやらブログなどを書いて、そのへんの知識をまとめていきたい。
  • 公式ドキュメントにテストの書き方的なのが記されてない。
  • 込み入ったことをやろうとすると、Express.js のドキュメントを読みまくる必要があったりする。

今のところ、0.10.0-rc7 が最新の状態なので、0.10 がリリースされたら、もう一度見てみたいかなと思ってます。

node.js で動くWebアプリを作り、mocha などでユニットテストを書きたいというシチューエーションになりました。

しかし、そのWebアプリは request によって外部コンテンツを
参照しているため、なかなかテストが書きにくいです。

そこで、探し出してきた便利ライブラリが nock です。

https://github.com/pgte/nock

このライブラリを使うと、特定のHTTPリクエストを mock することが
できるようです。
下のようなテストコードが、パスします。

var request = require('request')
    , nock = require('nock')
    , assert = require('assert');

describe('http request', function() {
  describe('get request', function() {
    it('should return {"hello": "world"}', function() {
      // mocking http request
      nock('http://www.example.com')
        .get('/data')
        .reply(200, {
          hello: 'world'
        }, {
          'Content-Type': 'application/json'
        }); 

      // request
      request('http://www.example.com/data', function(err, response, body) {
        assert.equal(err, null);
        assert.equal(response.statusCode, 200);
        assert.equal(body, '{"hello":"world"}');
      }); 
    }); 
  }); 
});

冬休みの自由研究として、 angular.js を触り始めましたのでメモです。

logo

こいつが、個人的にはなかなか好みです。

  • Dependency Injection がある
  • テストの書き方とかのガイドもしっかり揃っている (英語だけど)
  • エラー時の手厚いサポート
  • RESTful な WebAPIとの親和性

というわけで、今後使っていくために、簡単なアプリを書いてみました。

https://github.com/kawahara/devnull
シンプルな一言メモ的なものです。
まだ、ACLなどを実装しておりませんので、実用なものではありません。

実際に動きを見たい場合は、node.js と mongodb を用意して、

npm install
node app.js

と言った感じで、起動し、 http://localhost:3000 などを見てください。

サーバサイドのアプリは、何を思ったのか、swig が使える設定が入っていますが、
これは最初の開発の試行錯誤の段階で使っていたものです。

実際には、テンプレーティングをいないものです。
サーバサイドのアプリが行っているのは、WebAPI の提供のみです。

今のところ以下のような、WebAPI を提供しています。

  • 投稿
    • GET /api/posts – 投稿一覧を取得する
    • POST /api/posts – 投稿する
    • GET /api/posts/:id – 投稿を取得する
    • DELETE /api/posts/:id – 投稿を削除する
  • 投稿に対するコメント
    • GET /api/posts/:postId/reply – 投稿に値するコメントを取得する
    • POST /api/posts/:postId/reply – 投稿に対してコメントする
    • DELETE /api/posts/:postId/reply/:id – 投稿に対するコメントを削除する

クライアントのアプリは、静的コンテンツである、public/index.html
angualr.js によって動作しているわけです。コントローラーのコードは、
public/devnull.js に書いています。

resource module を使うと、RESTful な WebAPI を簡単に利用することができます。

HTML側では、angular.js のパッケージに同梱されている angular-resource.min.js を
ロードしておきます。

ではサービスの定義。

var postsServices = angular.module('postsServices', ['ngResource']);
postsServices.factory('Post', ['$resource',
  function($resource) {
    // $resource オブジェクトを作成, 第2引数はデフォルトのパラメータオブジェクト
    // 第3引数はアクションを定義したオブジェクト
    return $resource('api/posts/:id', {}, {
      // 取得 (配列で取得する)
      query: {method: 'GET', isArray: true},
      // 追加は POST で
      save: {method: 'POST'},
      // 削除は DELETE で
      remove: {method: 'DELETE'}
    });
  }
]);

postsServices.factory('Reply', ['$resource',
  function($resource) {
    return $resource('api/posts/:postId/reply/:id', {}, {
      save: {method: 'POST'},
      remove: {method: 'DELETE'}
    });
  }
]);

これらのサービスを、コントローラーで使います。

var devnullApp = angular.module('devnullApp', ['postsServices']);
devnullApp.controller('postsCntl', ['$scope','Post','Reply',
  function($scope, Post, Reply) {
     // controller
     // Post や Reply リソースを利用できる
  }
])

コントローラーで Post.query() を実行すると、 GET api/posts でデータの取得が行われ、
POSTリソースのオブジェクトの配列となって利用できます。
この時、URIは、 api/posts/:id と宣言していますが、id を指定しない場合は、
省略され、 api/posts という形になるようです。

新しく Post を投稿する際は、Post リソースのオブジェクトを新しく作成し、
$save() メソッドを利用します。

var post = new Post({body: "Hello, world."});
post.$save(function(savedObject, handler) {
  // 保存後の処理
});

IDなどを別途指定する必要がある場合は、リソースのアクションメソッド
の第1引数に渡します。

post.$remove({id: "123"}, function() {
  // 削除後の処理
});

もちろん、これに対するテストを書いていきます。

まず、公式ドキュメントの通り、karma を利用します。対象のテストや実装が更新されると
自動的にテストを流してくれたりする、素晴らしいツールです。

npm install -g karma karma-jasmine karma-junit-reporter karma-chrome-launcher karma-firefox-launcher karma-script-launcher

config/karma.config.js を用意して、karma を起動します。
コンソールに出たURLを、ブラウザで見ると、テストやスクリプトが更新された際に、
全自動でテストが実行されます。

karma start config/karma.conf.js
INFO [karma]: Karma v0.10.9 server started at http://localhost:9876/

通常、WebAPIを利用したアプリのテストを書くのは面倒に思えますが、
angualr.js では http request 周りを Mock することができる、 $httpBackend を備えています。

これは、特定のリクエストが行われたかを確認したり、
そのレスポンスを自分が指定したものを返すといったことができます。

test/unit/controllersSpec.js に jasmine 用のテストケースを用意しました。

$httpBackend の基本的な使い方としては、

  1. 予測されるリクエストについて書く
  2. アクションを実行
  3. $httpBackend.flush() を実行。この時点でHTTP Request が実際に行われる状態になる。(そもそも、そのようなコールがないような状態だと、テストがコケる。)

という流れになります。

今回のアプリは、ページを開いた際に Post.query() が実行されるようなものになっているので、
GET api/posts がコールされることが予測されます。

なので、

    beforeEach(inject(function(_$httpBackend_,$rootScope, $controller) {
      $httpBackend = _$httpBackend_;

      $httpBackend.expectGET('api/posts')
        .respond(200, postsData);

      scope = $rootScope.$new();
      ctrl = $controller('postsCntl', {$scope: scope});
    }));

のように、api/posts に関しては、200と、予め用意したテストデータを返すようにします。
(これは全てのテストケースについて定義したいので、beforeEach 内に書きます。)

そのうえで、

    it('should display post list', function() {
      expect(scope.posts).toEqualData([]);
      $httpBackend.flush();
      expect(scope.posts).toEqualData(postsData);
    });

$httpBackend.flush() が実行されたあと、正しくデータが scope にセットされているか。
というのを確かめるというテストが書けます。

    it('should display a new post after the submit action', function() {
      var submitData = {
        body: "Hello World."
      };

      $httpBackend.expectPOST('api/posts')
        .respond(201, "");
      scope.post = submitData;
      scope.submit();
      $httpBackend.flush();

      expect(scope.post).toEqualData(null);
      expect(scope.posts[0]).toEqualData(submitData);
    });

投稿のテストも、同様に expectPOST() を使うことで書くことができます。