LoginSignup
23

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-08-13

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)

フォルダ構成
├── 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

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>
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!");
  };
});
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で行います。

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に対するテストを書いて終わりです。

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の単体テストを書く
JasmineのSpy機能でテストダブルを作成する
Unit Testing $http service in Angular.js
Alert! Testing Javascript's alert function with Jasmine

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
23