AngularJSでプロトタイピングのススメ

こんにちは。今日はクリスマスですね(^^)
日本人プログラマにとっては忙しい時期だと思いますが・・・(^^;

注：本記事は都合により最新ではないAngularJS 1.2系をベースとしています。おそらく1.3系でも同様に動作すると思うのですが、未確認です。ごめんなさいm(_ _)m

## こんな問題ないですか？

### 製品がほぼ完成した後に顧客から「これじゃない」と言われる

外部設計書やhtmlモックで確認して貰っていたにも関わらず、クライアント側、サーバー側実装完了した段階で顧客に触ってもらったら「この動きは想定していたのと違う」と言われたり。

イテレーションを短くすればある程度手戻りは防げますが、それでももう少し早い段階でちゃんと確認して欲しいと思う事もあります。

## で、プロトタイピング

クライアント側をhtml+jsでリッチにしてサーバー側とのやりとりをRESTなどのAPIベースに作ることで、サーバー側APIをうまくモックすればクライアント側だけのプロトタイプを作成することが出来ます。

クライアント側のプロトタイプが先に出来上がればサーバー側の開発に入る前に顧客に動きを確認して貰うことが出来ます。承認得ておけば、後から「こんなんじゃなかった」とも言われなくなります。

## AngularJSでプロトタイピング

サーバー側APIモックを作るツールは世の中たくさんあると思いますが、AngularJSに含まれている機能で結構簡単にモックを作れたので紹介したいと思います。

またAngularJSでプロトタイプを作ると、オマケでいいことがあります。

### プログラマとデザイナ（またはhtmlコーダ）の協業が楽に

以前はView層の開発にJava Servlet/JSPをよく使っていましたが、プログラマがデザイナの作ったhtmlをJSPに落とす作業で非常に苦労していました。

最初の変換はまだよいですが、あとから変更が入った場合にも差分を一つ一つチェックしてJSPにマージしていく必要があり、しょっちゅうデグレを起こしていました。

View層をhtml+jsで作る様になってからも同様で、デザイナが条件によって異なるレイアウトのページを全てhtmlで作成しプログラマがそれをjs入りのhtmlに落とすという面倒な作業をしていました。

AngularJSでプロトタイプ作ると、デザイナとプログラマが同じhtmlファイルを編集出来る様になります。

AngularJSで書かれたhtmlはJSにあまり明るくないデザイナにも結構受け入れてもらいやすかった気がします。[^1]
また、条件によって変わるレイアウトも、それぞれ別にデザインを作らずに直接プロダクションhtmlを修正出来る様になります。

[^1]: 作り方にもよりますが。。

### 作り直す必要がない

使い捨てプロトタイプと違いそのままプロダクションコードとして動作します。プロトタイプが完成した時点で後はサーバー側実装を残すのみとなります。

また、サーバー側APIレベルでモックを作成するのでプロトタイプ完成した時点でサーバー側の仕様がある程度まとまっています。プロトタイプ作成と同時にサーバー側API設計にも繋がるので一石二鳥です。

## 手順

AngularJSのngMockE2Eモジュールの`$httpBackend`サービスを使います。
https://code.angularjs.org/1.2.28/docs/api/ngMockE2E/service/$httpBackend
※ ngMockモジュールにも同様の$httpBackendがあるので注意。そちらはユニットテスト用。

ngMockE2Eはそもそもe2eテスト用のサーバーサイドモックを作る為の機能ですが、プロトタイプ用のモックを作るのにもとても便利です。

`$http`や、`$resource`の裏側のhttp通信をごっそりモックしてくれます。

### angular-mocks.jsの読み込み

AngularJSに標準で同梱されているangular-mocks.jsを読み込みます。

読み込み方は何でもよいですが、私はプロダクション環境で余計なリクエストが飛ぶのが嫌で、下記の様にしました。

```js
some.namespace.importMockJs = function(jsfile) {
  if (location.hostname === 'mockhost') {
    document.write('<script type="text/javascript" src="' + jsfile + '"></script>');
  }
};
some.namespace.importMockJs('/js/angular-mocks.js');
```

予めhostsファイルに「`127.0.0.1 mockhost`」としておいて、ホスト名mockhostでアクセスされた場合のみモックが有効になる様にしました。

今考えるとgruntなどのビルドツールで切り替えるのが正しかった気もします。。(^^;

### モック処理JSの読み込み

モックファイルを読み込みます。
下記コード例ではコントローラー単位でモックファイルを作っています。アプリの規模が小さければ全体で一つにしてもよいと思います。

```js:some-controller.js
(function () {
  var app = angular.module('someApp');
  app.controller('SomeController', function($scope) {
    // (snip)
  });
}());
some.namespace.importMockJs('/jsmock/aaa/bbb/ccc/someController.mock.js');
```

### モック処理JSの記述

$httpBackendにwhenGET, whenPOST, ...when<その他httpメソッド>などのメソッドがあるので、任意のURLに対する呼び出しのレスポンスをモック処理を記述します。

ここではいくつかのパターンの記述例を紹介します。
簡単な説明はコメント中に書きました。

```js:someController.mock.js
(function () {
  // おまじない
  // htmlタグについたng-app属性を動的に書き換える
  // ここではjQuery使ってますが、DOMベタ書きでも構いません。
  $('html[ng-app]').attr('ng-app', function(arr) {
    // suffix 'Mock'付けてる
    return $(this).attr('ng-app') + 'Mock';
  });

  // モックappを作成して登録（名前は対象app名 + 'Mock'）
  // requiresに対象appと'ngMockE2E'を指定
  var appMock = angular.module('someAppMock', ['someApp', 'ngMockE2E']);
  appMock.run(['$httpBackend', function($httpBackend) {

    // ここからモック処理記述例
    // 基本的にe2eテストと同様

    // 正規表現にマッチするパスに対してXHRがあったらモックレスポンス(JSON)を返却 
    $httpBackend.whenGET(/\/api\/checkLogin\?.*/).respond(
      {result: 'success', accountType: 'USER', accountName: '株式会社ほげ'}
    );

    // array JSONを返却
    $httpBackend.whenGET(/\/api\/accounts\?.*/).respond(
      [
        {key:'1', accountName:'ほげほげ'},
        {key:'2', accountName:'ふがふが'},
        ...(snip)...
      ]
    );

    // POSTで、固定URLに対するモック
    $httpBackend.whenPOST('/api/report').respond(
      {jobId: 'dummy', applicationKey: 'dummyApp', date: '2014-12-25'}
    );

    // respondにfunctionを渡すことで動的にレスポンスをコントロールすることが出来る
    // 下記例では最初はdone:falseを返し、リトライ後にdone: trueを返している
    var retry = 0;
    $httpBackend.whenPOST('/api/checkJob').respond(function(method, url, data) {
      if (retry++ >= 1) {
        retry = 0;
        // array [ステータスコード, JSONレスポンス]をreturnする
        return [200, {done: true}];
      } else {
        return [200, {done: false}];
      }
    });

    // pathThroughを呼び出すとmockではなく本来のXHRが送信される（静的htmlのincludeなど）
    $httpBackend.whenGET(/\/html\/.*/).passThrough();
  }]);
}());
```

$httpBackendの使い方はe2eテストと同様です。
もっと詳細な利用方法知りたい人は下記参照して下さい。
https://code.angularjs.org/1.2.28/docs/api/ngMockE2E/service/$httpBackend

## 注意

### 最初の要件定義には使わない

この手法で作ったプロトタイプはユーザーから見て完成品の様に動作する為、要件を確認するのに便利です。
とは言え、クライアントサイドは作り込むのでそれなりにコストがかかります。初期の要件定義にはペーパープロトタイピング等のもっと軽い手法がよいかと思います。

### モック作り込みすぎない

モックを作っていると、だんだん動くものが出来て行って結構楽しいので、ついつい作り込んでしまいます。いつのまにか表面的には完成品の様に動くものが出来てしまったり。。

でもモックを作ることが目的ではありません。モックは最終的に捨てられるものです。
以下の目的を満たす最低限のコストで作りましょう。

- レイアウトが変更する最低限のパターンの準備
- 顧客に確認してもらいたい要件が見える様に

あまり作り込み過ぎると顧客に「なんだ、もう殆ど完成してるじゃないか！」という誤解を与える危険性もありますので、ほどほどにモック感を残しておくのも大事かと思います:p

## その他

弊社では以下の様なツールも自作して使っています。

- デザイナや顧客向けにモックの条件やヒントをバルーン表示
- Google Spreadsheetからmock用テストデータJSON mapを生成

この記事では割愛しますが、またの機会に紹介したいと思います。

## 最後に

ここで紹介した手法は現在も試行錯誤している自己流です。「こうした方がいいんじゃないか」とか、「もっといいツールあるよ」とかあったら教えて頂けるととても嬉しいです。

ではでは。よいお年を〜＼(^o^)／