Jasmineのみを使用したAngularJSのテスト

# AngularJSのテスト
AngularJSのUnitテストは開発者ガイドではJasmineが紹介されています。
https://docs.angularjs.org/guide/unit-testing

さらに、チュートリアルではJamineの実行環境としてkarmaのインストール方法が紹介されていますが、
karmaをインストールするにはnode.jsが必要なようです。
https://docs.angularjs.org/tutorial

わけあってnodeをインストールしたくなかったので、Jasmineのみ（スタンドアローン）でAngularJSのテストを
トライしてみました。
(Jasmine-2.0.1: https://github.com/pivotal/jasmine/blob/master/dist/jasmine-standalone-2.0.1.zip)

```lang:フォルダ構成
├── angular
│   ├── angular-cookies.js
│   ├── angular-mocks.js
│   ├── angular-resource.js
│   ├── angular-route.js
│   ├── angular-sanitize.js
│   └── angular.js
├── app
│   ├── controllers.js
│   └── services.js
├── index.html
└── jasmine
    ├── MIT.LICENSE
    ├── SpecRunner.html
    ├── lib
    │   └── jasmine-2.0.1
    │       ├── boot.js
    │       ├── console.js
    │       ├── jasmine-html.js
    │       ├── jasmine.css
    │       ├── jasmine.js
    │       └── jasmine_favicon.png
    └── spec
        └── sampleControllerSpec.js

```

テスト対象はAngularJSの開発者ガイドから拝借。
（YahooファイナンスAPIで取得した各国の為替レートを入力した数量・金額に掛けて一覧表示するもの）
https://docs.angularjs.org/guide/concepts

```html:index.html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Try Jasmine</title>
    <script src="angular/angular.js"></script>
    <script src="angular/angular-resource.js"></script>
    <script src="angular/angular-cookies.js"></script>
    <script src="angular/angular-sanitize.js"></script>
    <script src="angular/angular-route.js"></script>
    <script src="app/app.js"></script>
    <script src="app/controllers.js"></script>
    <script src="app/services.js"></script>
  </head>
  <body ng-app="myapp.controllers">
    <h1>AngularJs Sample</h1>
    <div class="container" ng-controller="SampleController">
      <b>Invoice:</b>
      <div>
        Quantity: <input type="number" ng-model="qty" required />
      </div>
      <div>
        Costs: <input type="number" ng-model="cost" required /><br />
        <select ng-model="inCurr">
          <option ng-repeat="c in currencies">{{c}}</option>
        </select>
      </div>
      <div>
        <b>Total:</b>
        <span ng-repeat="c in currencies">
          {{total(c) | currency:c }}
        </span><br/>
        <button class="btn" ng-click="pay()">Pay</button>
      </div>
    </div>
</body>
</html>
```
```js:controllers.js
angular.module('myapp.controllers', ['myapp.services'])
.controller('SampleController', function($scope, SampleService) {
  $scope.qty = 1;
  $scope.cost = 2;
  $scope.inCurr = 'JPY';
  $scope.currencies = SampleService.currencies;
  $scope.total = function (outCurr) {
    return SampleService.convert($scope.qty * $scope.cost, $scope.inCurr, outCurr);
  };
  $scope.pay = function () {
    window.alert("Thanks!");
  };
});
```
```js:services.js
angular.module('myapp.services', [])
.factory('SampleService', function ($http) {
  var YAHOO_FINANCE_URL_PATTERN = 
    'http://query.yahooapis.com/v1/public/yql?q=select * from '+
      'yahoo.finance.xchange where pair in ("PAIRS")&format=json&'+
        'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
  var currencies = ['USD', 'EUR', 'CNY', 'JPY'];
  var usdToForeignRates = {};
  refresh();
  return {
    currencies: currencies,
    convert: convert,
    refresh: refresh
  };
  function convert(amount, inCurr, outCurr) {
    return amount * usdToForeignRates[outCurr] * 1 / usdToForeignRates[inCurr];
  };

  function refresh() {
    var url = YAHOO_FINANCE_URL_PATTERN.replace('PAIRS', 'USD' + currencies.join('","USD'));
    return $http.jsonp(url).success(
      function(data) {
        var newUsdToForeignRates = {};
        angular.forEach(data.query.results.rate, function(rate) {
          var currency = rate.id.substring(3,6);
          newUsdToForeignRates[currency] = window.parseFloat(rate.Rate);
        });
        usdToForeignRates = newUsdToForeignRates;
      });
  };
});
```

# テストする
使用するライブラリの定義は`SpecRunner.html`で行います。

```html:SpecRunner.html
<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Jasmine Spec Runner v2.0.1</title>

  <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.0.1/jasmine_favicon.png">
  <link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.1/jasmine.css">

  <script type="text/javascript" src="lib/jasmine-2.0.1/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-2.0.1/jasmine-html.js"></script>
  <script type="text/javascript" src="lib/jasmine-2.0.1/boot.js"></script>
  <script type="text/javascript" src="../angular/angular.js"></script>
  <script type="text/javascript" src="../angular/angular-mocks.js"></script>
  <script type="text/javascript" src="../angular/angular-resource.js"></script>
  <script type="text/javascript" src="../angular/angular-cookies.js"></script>
  <script type="text/javascript" src="../angular/angular-sanitize.js"></script>
  <script type="text/javascript" src="../angular/angular-route.js"></script>


  <!-- include source files here... -->
  <script type="text/javascript" src="../app/controllers.js"></script>
  <script type="text/javascript" src="../app/services.js"></script>

  <!-- include spec files here... -->
  <script type="text/javascript" src="spec/SampleControllerSpec.js"></script>

</head>

<body>
</body>
</html>
```
sourceやspecをファイルの分だけ定義しなくてはならないのが面倒くさいです。
ライブラリはほとんどindex.htmlで定義するものと同じでよいのですが、唯一テスト用に
`angular-mocks.js`
を定義してあげることが必要です。
定義していない場合は`ReferenceError: module is not defined`という結果が表示されます。
あまり直接的でないエラーメッセージなので嵌りました。

あとは`SampleController`に対するテストを書いて終わりです。

```js:sampleControllerSpec.js
'use strict';

describe('Controller: SampleController', function () {

  // load the controller's module
  beforeEach(module('myapp.controllers'));

  var ctrl,
    scope;

  var expected = [ 'USD', 'EUR', 'CNY', 'JPY' ];
  var usdToForeignRates = [];
  usdToForeignRates['USD'] = 0.01;
  usdToForeignRates['EUR'] = 0.01;
  usdToForeignRates['CYN'] = 0.06;
  usdToForeignRates['JPY'] = 1.00;

  var alert_msg;

  // Initialize the controller and a mock scope
  beforeEach(inject(function ($controller, $rootScope, SampleService) {
    scope = $rootScope.$new();
    ctrl = $controller('SampleController', {
      $scope: scope
    });
    spyOn(SampleService, 'convert').and.callFake(function(amount, inCurr, outCurr) {
      return amount * usdToForeignRates[outCurr] * 1 / usdToForeignRates[inCurr];
    });
    alert_msg = '_defalut_';
    spyOn(window, 'alert').and.callFake(function(msg) {
      alert_msg = msg;
    });
  }));

  it('should attach a list of currencies to the scope', function () {
    expect(scope.currencies.length).toBe(4);
    expect(scope.currencies).toEqual(expected);
  });

  it('when total() is called returns the calculated value', function () {
    expect(scope.total('USD')).toBe(0.02);
    expect(scope.total('EUR')).toBe(0.02);
    expect(scope.total('CYN')).toBe(0.12);
    expect(scope.total('JPY')).toBe(2);
  });

  it('when pay() is called returns the correct alert message', function () {
    expect(alert_msg).toEqual('_defalut_');
    scope.pay();
    expect(alert_msg).toEqual('Thanks!');
  });
});
```

簡単に説明しますと、
まず例にならって、最初の`beforeEach`で使用するモジュールを定義します。
次の`beforeEach`では`scope`とテスト対象コントローラー`ctrl`を生成します。

さらに、ここで`SampleService`の関数`convert`にはJasmineのSpy機能を使って代役オブジェクトを定義しました。
これは`convert`が外部の処理を呼び出した結果を用いているためです。
また、alertのメッセージ確認にもSpy機能を使用しています。Spyちゃん便利ちゃん。

# 参考
・[AngularJSの単体テストを書く](http://qiita.com/opengl-8080/items/f150dc723509b2871d93)
・[JasmineのSpy機能でテストダブルを作成する](http://www.buildinsider.net/web/bookjslib111/109)
・[Unit Testing $http service in Angular.js](http://jasonmore.net/unit-testing-http-service-angular-js/)
・[Alert! Testing Javascript's alert function with Jasmine](http://blog.q-media.com/2013/02/alert-testing-javascripts-alert.html)