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
<!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>
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!");
};
});
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
で行います。
<!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
に対するテストを書いて終わりです。
'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