Interceptor
Interceptorを使用すると、サーバに引き渡される前のリクエストの前処理や、リクエストを行ったアプリケーションコードに引き渡される前のレスポンスの前処理、さらにはグローバルのエラー処理を定義することができるようです。
https://docs.angularjs.org/api/ng/service/$http #Interceptors
Interceptorを利用して、リクエストからレスポンスまでの時間の測定と、レスポンスのエラーハンドリングにトライしてみました。
├── angular
│ ├── angular-cookies.js
│ ├── angular-resource.js
│ ├── angular-route.js
│ ├── angular-sanitize.js
│ └── angular.js
├── app
│ ├── controllers.js
│ ├── services.js
│ └── configurations.js ← 新規追加
└── index.html
サンプルソースはAngularJSの開発者ガイドから拝借。
(YahooファイナンスAPIで取得した各国の為替レートを入力した数量・金額に掛けて一覧表示するもの)
https://docs.angularjs.org/guide/concepts
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Try Interceptor</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 () {
SampleService.pay($scope.qty, $scope.incurr);
};
});
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, status, headers, config) {
var time = config.responseTimestamp - config.requestTimestamp;
console.log('The request took ' + (time / 1000) + ' seconds.');
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;
}
);
};
function pay(qty, incurr) {
$http.post("/test", {qty: qty, incurr: incurr});
}
});
Interceptorの処理はconfigurations.jsで行いました。
angular.module('myapp.configurations', ['myapp.controllers'])
.config(function($httpProvider) {
$httpProvider.interceptors.push(
function ($q, $rootScope) {
return {
request: function(config) {
config.requestTimestamp = new Date().getTime();
return config;
},
response: function(response) {
response.config.responseTimestamp = new Date().getTime();
return response;
},
responseError: function(rejection) {
if (500 == rejection.status) {
alert('System Error!');
}
return $q.reject(rejection);
}
};
}
);
});
Interceptorの定義は$httpProvider
がもつ配列interceptors
にfactoryをpushするだけで行えます。
interceptors
にpushするfactoryに定義できるfunctionは以下の4つ。
-
request
: サーバに引き渡される前のリクエストの前処理を定義 -
response
: リクエストを行ったアプリケーションコードに引き渡される前のレスポンスの前処理 -
requestError
: リクエストでエラーが起きた際の処理を定義 -
responseError
: レスポンスでエラーが起きた際の処理を定義
(see https://docs.angularjs.org/api/ng/service/$http #Interceptors)
本稿ではリクエストからレスポンスまでの時間の測定と、レスポンスのエラーハンドリングを行うため、request
、response
とresponseError
を定義しました。
request
、response
では処理時の時間を算出するように、
responseError
はHTTPステータスコード500 の場合に処理をするようにしています。
実行してみる
リクエストからレスポンスまでにかかる時間はコンソールに出力されるはずなので確認してみると、
と表示されていることがわかります。
エラーハンドリングのテストをする
エラーハンドリングの確認はサーバーサイドを書いてもいいのですが、面倒なのでテストで確認してみます。
基本的なテストの書き方は以下を参照してください。
Jasmineのみを使用したAngularJSのテスト
AngularJSではバックエンドの振る舞いをMock化する$httpBackendを提供しています。
https://code.angularjs.org/1.2.26/docs/api/ngMock/service/$httpBackend
これを利用してSampleService
のpay
メソッドで行っている$http.post
のサーバーサイドをMock化しました。
※SampleService読み込み時に$http.jsonp
でyahooapiにアクセスしてしまうのでこれもMock化しています。
'use strict';
describe('Service: SampleService', function () {
beforeEach(module('myapp.services'));
var $httpBackend,
SampleService ;
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 alert_msg;
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
var currencies = ['USD', 'EUR', 'CNY', 'JPY'];
var url = YAHOO_FINANCE_URL_PATTERN.replace('PAIRS', 'USD' + currencies.join('","USD'));
$httpBackend.when('JSONP', url).respond(
{query: {results: {rate: [
{id: 'HOGE', rate: 1.0}
]}}}
);
alert_msg = '_defalut_';
spyOn(window, 'alert').and.callFake(function(msg) {
alert_msg = msg;
});
SampleService = $injector.get('SampleService');
}));
it('should success, when pay() is called', function() {
expect(alert_msg).toEqual('_defalut_');
var data = {qty: 1, cost: 1000};
$httpBackend.when('POST', '/test', data).respond(200, '');
SampleService.pay(1, 1000).success(function() {
expect(true).toBe(true);
}).error(function() {
expect(false).toBe(true);
});
$httpBackend.flush();
expect(alert_msg).toEqual('_defalut_');
});
it('should fail, when pay() is called', function() {
expect(alert_msg).toEqual('_defalut_');
var data = {qty: 1, cost: 1000};
$httpBackend.when('POST', '/test', data).respond(400, '');
SampleService.pay(1, 1000).success(function() {
expect(false).toBe(true);
}).error(function() {
expect(true).toBe(true);
});
$httpBackend.flush();
expect(alert_msg).toEqual('_defalut_');
});
it('should fail and emit alert message for status code 500, when pay() is called', function() {
expect(alert_msg).toEqual('_defalut_');
var data = {qty: 1, cost: 1000};
$httpBackend.when('POST', '/test', data).respond(500, '');
SampleService.pay(1, 1000).success(function() {
expect(false).toBe(true);
}).error(function() {
expect(true).toBe(true);
});
$httpBackend.flush();
expect(alert_msg).toEqual('System Error!');
});
});
テストケースは3つ。
- ステータスコード200の正常系
- ステータスコード400の異常系
- ステータスコード500の異常系
alertメッセージの中身を確認しています。
参考
・Interceptors in AngularJS and Useful Examples
・AngularJS を本気でつかうための tips