Javascript 使用success()和error()测试控制器
我正试图找出在控制器中实现单元测试成功和错误回调的最佳方法。我可以模拟服务方法,只要控制器只使用默认的$q函数,比如“then”(参见下面的示例)。当控制器响应“成功”或“错误”承诺时,我遇到问题。(如果我的术语不正确,很抱歉) 下面是一个示例控制器\服务Javascript 使用success()和error()测试控制器,javascript,angularjs,unit-testing,jasmine,Javascript,Angularjs,Unit Testing,Jasmine,我正试图找出在控制器中实现单元测试成功和错误回调的最佳方法。我可以模拟服务方法,只要控制器只使用默认的$q函数,比如“then”(参见下面的示例)。当控制器响应“成功”或“错误”承诺时,我遇到问题。(如果我的术语不正确,很抱歉) 下面是一个示例控制器\服务 var myControllers = angular.module('myControllers'); myControllers.controller('SimpleController', ['$scope', 'myService'
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);
});
});
第一个测试通过,第二个测试失败,错误为“TypeError:对象不支持属性或方法‘success’”。在这种情况下,我得到的结果是,我保证
没有成功的功能。好的,问题是,编写这个测试的好方法是什么,这样我就可以测试模拟服务的“成功”、“错误”和“然后”条件
我开始认为我应该避免在控制器中使用success()和error()
编辑
因此,经过进一步思考,并感谢下面的详细答案,我得出结论,在控制器中处理成功和错误回调是不好的。正如HackedByChinese在下面提到的,成功\错误是由$http添加的语法糖。所以,实际上,通过尝试处理success\error,我让$http关注点泄漏到我的控制器中,这正是我试图通过将$http调用包装到服务中来避免的。我将采取的方法是将控制器更改为不使用success\error:
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()来测试error\success条件:
'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
添加的语法糖,因此当你创建自己的承诺时,它们并不存在。您有两个选择:
1-不要模拟服务并使用$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');
});
});
});
2-返回一个完全被嘲笑的承诺
如果您正在测试的东西具有复杂的依赖关系,并且所有的设置都是令人头痛的,那么您可能仍然希望在尝试时模拟服务和调用本身。不同的是,你会想要完全模仿承诺。这样做的缺点是可以创建所有可能的模拟承诺,但是您可以通过创建自己的函数来创建这些对象,从而使这变得更容易
之所以这样做,是因为我们假装它通过调用success
、error
或提供的处理程序来解决问题,然后立即执行,从而使它同步完成
'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
,但是将直接使用解析的响应数据调用,而不是响应对象(因此它应该是data
,而不是response.data
)。不要混淆关注点!
在控制器中使用$httpBackend
是一个坏主意,因为您在测试中混用了各种问题。是否从端点检索数据不是控制器的问题,而是您正在调用的数据服务的问题
如果更改服务内的端点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;
}
通过调用此函数,您将获得一个真正的承诺,在需要时响应然后
和捕获
方法,并将为成功
或错误
回调工作。请注意,成功和错误本身返回一个承诺,因此它将使用链式然后方法
(注意:在第4行和第6行,函数在对象的数据属性中返回resolve和reject值。这是为了模拟$http的行为,因为它返回数据、http状态等。)是的,不要在控制器中使用$httpbackend,因为我们不需要发出真正的请求,您只需要确保一个单元完全按照预期工作,看看这个简单的控制器测试,它很容易理解
/**
* @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
});
});
});
}());
对于角度模拟(您已经在使用),可以使用$httpBackend
设置http调用的期望值并指定它们应该如何响应,然后可以刷新它们以同步完成测试。为什么不走那条路呢?看:谢谢,但我看不出这有什么帮助。在本例中,我尝试对控制器进行单元测试,因此我完全模拟了进行$http调用的服务。在本例中,当模拟服务被注入控制器时,没有调用$http的代码。感谢您教我将http功能从控制器中分离出来,现在将其放在控制器中是没有意义的,因为我已经看到了这一点。另外,在Jasmine 2.3.1中,您需要在spy上使用和.returnValue
,我将如何测试错误条件,而不是andReturn
感谢您的回答?我开始认为在控制器之外显式地使用成功和错误确实是个坏主意。我想控制员公司