Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
24
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

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)

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
24
Help us understand the problem. What are the problem?