コントローラで成功とエラーのコールバックを単体テストするための最良の方法を模索しています。コントローラが 'then'などのデフォルトの$ q関数のみを使用している限り、サービスメソッドをモックアウトできます(以下の例を参照)。コントローラーが「成功」または「エラー」の約束に応答すると問題が発生します。 (私の用語が正しくない場合は申し訳ありません)。
これはコントローラ\サービスの例です
var myControllers = angular.module('myControllers');
myControllers.controller('SimpleController', ['$scope', 'myService',
function ($scope, myService) {
var id = 1;
$scope.loadData = function () {
myService.get(id).then(function (response) {
$scope.data = response.data;
});
};
$scope.loadData2 = function () {
myService.get(id).success(function (response) {
$scope.data = response.data;
}).error(function(response) {
$scope.error = 'ERROR';
});
};
}]);
cocoApp.service('myService', [
'$http', function($http) {
function get(id) {
return $http.get('/api/' + id);
}
}
]);
次のテストがあります
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var controller;
var getResponse = { data: 'this is a mocked response' };
beforeEach(angular.mock.module('myApp'));
beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams){
scope = $rootScope;
var myServiceMock = {
get: function() {}
};
// setup a promise for the get
var getDeferred = $q.defer();
getDeferred.resolve(getResponse);
spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);
controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });
}));
it('this tests works', function() {
scope.loadData();
expect(scope.data).toEqual(getResponse.data);
});
it('this doesnt work', function () {
scope.loadData2();
expect(scope.data).toEqual(getResponse.data);
});
});
最初のテストは成功し、2番目のテストはエラー「TypeError:Object does not support property or method 'success'」で失敗します。この例では、getDeferred.promiseに成功関数がないことがわかります。さて、ここで質問です。偽のサービスの「成功」、「エラー」、および「その後」の状態をテストできるように、このテストを作成する良い方法は何ですか?
コントローラでのsuccess()およびerror()の使用を避けるべきだと私は考え始めています...
[〜#〜]編集[〜#〜]
したがって、これについてもう少し考えた後、以下の詳細な回答のおかげでコントローラでの成功とエラーのコールバックの処理が悪いという結論に達しました HackedByChineseが以下の成功について言及しているように\エラーは、$ httpによって追加される構文糖です。したがって、実際には、成功\エラーを処理しようとすることで、$ httpの懸念をコントローラーにリークさせています。これは、$ http呼び出しをサービスにラップすることで回避しようとしていたこととまったく同じです。私が取ろうとしているアプローチは、成功\エラーを使用しないようにコントローラを変更することです:
myControllers.controller('SimpleController', ['$scope', 'myService',
function ($scope, myService) {
var id = 1;
$scope.loadData = function () {
myService.get(id).then(function (response) {
$scope.data = response.data;
}, function (response) {
$scope.error = 'ERROR';
});
};
}]);
このように、遅延オブジェクトでresolve()およびreject()を呼び出すことにより、エラー\成功条件をテストできます。
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var controller;
var getResponse = { data: 'this is a mocked response' };
var getDeferred;
var myServiceMock;
//mock Application to allow us to inject our own dependencies
beforeEach(angular.mock.module('myApp'));
//mock the controller for the same reason and include $rootScope and $controller
beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams) {
scope = $rootScope;
myServiceMock = {
get: function() {}
};
// setup a promise for the get
getDeferred = $q.defer();
spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);
controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });
}));
it('should set some data on the scope when successful', function () {
getDeferred.resolve(getResponse);
scope.loadData();
scope.$apply();
expect(myServiceMock.get).toHaveBeenCalled();
expect(scope.data).toEqual(getResponse.data);
});
it('should do something else when unsuccessful', function () {
getDeferred.reject(getResponse);
scope.loadData();
scope.$apply();
expect(myServiceMock.get).toHaveBeenCalled();
expect(scope.error).toEqual('ERROR');
});
});
誰かが削除された回答で述べたように、success
とerror
は$http
によって追加された構文上の砂糖であるため、独自のプロミスを作成するときには存在しません。次の2つのオプションがあります。
$httpBackend
を使用して期待値を設定し、フラッシュしますアイデアは、テストされていることを知らずにmyService
を通常どおりに動作させることです。 $httpBackend
を使用すると、期待と応答を設定し、それらをフラッシュして、テストを同期的に完了することができます。 $http
は賢くならず、それが返すプロミスは実際のものと同じように見え、機能します。このオプションは、HTTPの期待がほとんどない単純なテストがある場合に適しています。
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var expectedResponse = { name: 'this is a mocked response' };
var $httpBackend, $controller;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$controller_, _$httpBackend_){
// the underscores are a convention ng understands, just helps us differentiate parameters from variables
$controller = _$controller_;
$httpBackend = _$httpBackend_;
scope = _$rootScope_;
}));
// makes sure all expected requests are made by the time the test ends
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('should load data successfully', function() {
beforeEach(function() {
$httpBackend.expectGET('/api/1').response(expectedResponse);
$controller('SimpleController', { $scope: scope });
// causes the http requests which will be issued by myService to be completed synchronously, and thus will process the fake response we defined above with the expectGET
$httpBackend.flush();
});
it('using loadData()', function() {
scope.loadData();
expect(scope.data).toEqual(expectedResponse);
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.data).toEqual(expectedResponse);
});
});
describe('should fail to load data', function() {
beforeEach(function() {
$httpBackend.expectGET('/api/1').response(500); // return 500 - Server Error
$controller('SimpleController', { $scope: scope });
$httpBackend.flush();
});
it('using loadData()', function() {
scope.loadData();
expect(scope.error).toEqual('ERROR');
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.error).toEqual('ERROR');
});
});
});
テストするものに複雑な依存関係があり、すべての設定が頭痛の種である場合でも、試行したとおりにサービスと呼び出し自体を模擬したい場合があります。違いは、約束を完全にあざける必要があるということです。これの欠点は、可能なすべての模擬プロミスを作成することですが、これらのオブジェクトを作成するための独自の関数を作成することで、これを簡単にすることができます。
これが機能する理由は、success
、error
、またはthen
によって提供されるハンドラーをすぐに呼び出して解決し、同期して完了するように装うためです。
'use strict';
describe('SimpleControllerTests', function () {
var scope;
var expectedResponse = { name: 'this is a mocked response' };
var $controller, _mockMyService, _mockPromise = null;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$controller_){
$controller = _$controller_;
scope = _$rootScope_;
_mockMyService = {
get: function() {
return _mockPromise;
}
};
}));
describe('should load data successfully', function() {
beforeEach(function() {
_mockPromise = {
then: function(successFn) {
successFn(expectedResponse);
},
success: function(fn) {
fn(expectedResponse);
}
};
$controller('SimpleController', { $scope: scope, myService: _mockMyService });
});
it('using loadData()', function() {
scope.loadData();
expect(scope.data).toEqual(expectedResponse);
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.data).toEqual(expectedResponse);
});
});
describe('should fail to load data', function() {
beforeEach(function() {
_mockPromise = {
then: function(successFn, errorFn) {
errorFn();
},
error: function(fn) {
fn();
}
};
$controller('SimpleController', { $scope: scope, myService: _mockMyService });
});
it('using loadData()', function() {
scope.loadData();
expect(scope.error).toEqual("ERROR");
});
it('using loadData2()', function () {
scope.loadData2();
expect(scope.error).toEqual("ERROR");
});
});
});
大きなアプリケーションであっても、オプション2を選択することはめったにありません。
価値があるのは、loadData
およびloadData2
httpハンドラーにエラーがあることです。それらはresponse.data
を参照しますが、 handlers は、応答オブジェクトではなく、解析された応答データで直接呼び出されます(したがって、response.data
ではなくdata
である必要があります) 。
コントローラー内で$httpBackend
を使用するのは悪い考えです。テスト内で懸念事項を混ぜ合わせているからです。エンドポイントからデータを取得するかどうかは、コントローラーの問題ではなく、呼び出すDataServiceの問題です。
サービス内のエンドポイントURLを変更すると、サービステストとコントローラーテストの両方のテストを変更する必要があるため、これをより明確に確認できます。
また、前述のように、success
とerror
の使用は構文上の砂糖であり、then
とcatch
の使用に固執する必要があります。しかし実際には、「レガシー」コードをテストする必要があることに気付くかもしれません。そのため、私はこの関数を使用しています:
function generatePromiseMock(resolve, reject) {
var promise;
if(resolve) {
promise = q.when({data: resolve});
} else if (reject){
promise = q.reject({data: reject});
} else {
throw new Error('You need to provide an argument');
}
promise.success = function(fn){
return q.when(fn(resolve));
};
promise.error = function(fn) {
return q.when(fn(reject));
};
return promise;
}
この関数を呼び出すことにより、必要なときにthen
およびcatch
メソッドに応答し、success
またはerror
でも機能するという真の約束が得られますコールバック。成功とエラーはpromise自体を返すため、チェーンされたthen
メソッドで動作することに注意してください。
はい、コントローラで$ httpbackendを使用しないでください。実際のリクエストを行う必要がないため、1つのユニットが期待どおりに機能していることを確認し、この単純なコントローラテストを確認するだけで簡単です。理解する
/**
* @description Tests for adminEmployeeCtrl controller
*/
(function () {
"use strict";
describe('Controller: adminEmployeeCtrl ', function () {
/* jshint -W109 */
var $q, $scope, $controller;
var empService;
var errorResponse = 'Not found';
var employeesResponse = [
{id:1,name:'mohammed' },
{id:2,name:'ramadan' }
];
beforeEach(module(
'loadRequiredModules'
));
beforeEach(inject(function (_$q_,
_$controller_,
_$rootScope_,
_empService_) {
$q = _$q_;
$controller = _$controller_;
$scope = _$rootScope_.$new();
empService = _empService_;
}));
function successSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve(employeesResponse);
return deferred.promise;
// shortcut can be one line
// return $q.resolve(employeesResponse);
});
}
function rejectedSpies(){
spyOn(empService, 'findEmployee').and.callFake(function () {
var deferred = $q.defer();
deferred.reject(errorResponse);
return deferred.promise;
// shortcut can be one line
// return $q.reject(errorResponse);
});
}
function initController(){
$controller('adminEmployeeCtrl', {
$scope: $scope,
empService: empService
});
}
describe('Success controller initialization', function(){
beforeEach(function(){
successSpies();
initController();
});
it('should findData by calling findEmployee',function(){
$scope.findData();
// calling $apply to resolve deferred promises we made in the spies
$scope.$apply();
expect($scope.loadingEmployee).toEqual(false);
expect($scope.allEmployees).toEqual(employeesResponse);
});
});
describe('handle controller initialization errors', function(){
beforeEach(function(){
rejectedSpies();
initController();
});
it('should handle error when calling findEmployee', function(){
$scope.findData();
$scope.$apply();
// your error expectations
});
});
});
}());