冬休みの自由研究として、 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() を使うことで書くことができます。

apple-touch-icon-144-precomposed

現在、このブログではオリジナルテンプレートとして、bootstrap を利用したものを
使っているのだが、レスポンシブ・ウェブデザイン対応をやるついでに、ノリで
新しいバージョンにあげちまうかー。という事になった。

2系と互換性があるかとおもったら、無いものが結構あったのでメモ。

気になった変更点とか

bootstrap-responsive.css は無くなった

どうやら、統合された模様。IE8は、別途外部JSファイルを呼ぶ必要がある模様。

span-* が無くなった

代わりに、col-*, col-sm-*, col-lg-* になりました。
レスポンシブ・ウェブデザインを意識しているのか、サイズ別にグリッドの大きさを
指定できる模様。

components の名前変更とか

hero-unit と呼ばれてたもの (トップページにでかでかと表示させるタイトル用のやつ)
は、 jumbotron というものに変更。

さらばIE7, Firefox3.6サポート

そして滅びよ

すべての変更を確認したいときは?

以下で、公開というか、管理されている模様。
https://github.com/twbs/bootstrap/pull/6342

gradle-icon-512x512

オレオレGradleメモだよー

お仕事ではGroovy 使いかつ、XMLをチマチマ書いていくのが嫌いなので、
Maven や Ant ではなく、Gradle をビルドに使っています。

Java プロジェクトの場合は、プロジェクトのルートに下のような
build.gradle を準備して、Maven と同じ ディレクトレイアウトを
作ってやるだけで良いというのは、なかなか簡単です。

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.+'
}

あとは依存追加したり、テストカバレッジを計測するために、
gradle-cobertura-plugin を導入したりしています。

課題

実装しているものの都合上、外部システムにHTTPでリクエストして
データを取得するというクラスが結構多いのですが、UnitTest の場合は
大抵、Mock にお願いする感じです。
(テストは Spock で書いていますので、Spock の Mock を活用する感じです。)

でも、たまに実際に、HTTPリクエストして返ってきたものを検証したい
という事もあり、HttpClient を Mock に差し替えず、実際に HTTP リクエストを
投げるテストも書いていたりします。

これを単純に src/test 下で書いてしまうと、 gradle build したときに、ネットワークや
データの都合などで見事にビルドが失敗したりします。
結果、Jenkinsのオッサンはその度に怒り狂い、平謝りをすることになります。

Jenkinsのオッサンには、UnitTest と、IntegrationTest的なものは
別々のタスクとして実行して貰いたい。ということで、新たにタスクを定義したい
わけです。

パッケージで分ける方法

とりあえず、ってことで今はパッケージで分ける方法を使っています。

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.+'
}

test {
    exclude 'net/bucyou/integration/**'
}

task integrationTest(type:Test) {
    include 'net/bucyou/integration/**'
}

これで、 net.bucyou.integration.* にあるテストは build で実行されなくなります。
一方、integrationTest タスクを対象のパッケージ下のみをテストするように定義
しました。これで分離出来ました。

プラグイン使用+新しいディレクトリ分類を作る

よくよく探すと、integration-test plugin がありました。

これを使うと、 src/integration-test/(java|groovy|scala|resources) 
integrationTest のためのディレクトリとして捉えたタスクを追加してくれるっぽいです。

最初からこれ使ってればよかった・・・。