J'essaie de trouver le meilleur moyen de réussir le test des unités et de rappeler les erreurs dans les contrôleurs. Je peux simuler des méthodes de service tant que le contrôleur utilise uniquement les fonctions $ q par défaut telles que 'then' (voir l'exemple ci-dessous). Je rencontre un problème lorsque le contrôleur répond à une promesse de "succès" ou "d'erreur". (Désolé si ma terminologie n'est pas correcte).
Voici un exemple de contrôleur\service
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);
}
}
]);
J'ai le test suivant
'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);
});
});
Le premier test réussit et le second échoue avec l'erreur "TypeError: l'objet ne prend pas en charge la propriété ou la méthode 'success'". Je comprends que dans cet exemple, getDeferred.promise N'a pas de fonction de succès. Ok, voici la question, quel est un bon moyen d'écrire ce test afin que je puisse tester les conditions de "succès", "erreur" et "puis" d'un service simulé?
Je commence à penser que je devrais éviter l'utilisation de success () et error () dans mes contrôleurs ...
MODIFIER
Donc, après avoir réfléchi un peu plus, et grâce à la réponse détaillée ci-dessous, je suis parvenu à la conclusion que la gestion des rappels de succès et d’erreur dans le contrôleur est mauvaise. Comme HackedByChinese mentionne ci-dessous success\error est le sucre syntaxique ajouté par $ http. Donc, en réalité, en essayant de gérer le succès\erreur, je laisse les préoccupations de $ http se fuir dans mon contrôleur, ce qui est exactement ce que j'essayais d'éviter en encapsulant les appels $ http dans un service. L’approche que je vais adopter consiste à changer le contrôleur pour ne pas utiliser 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';
});
};
}]);
De cette manière, je peux tester les conditions d'erreur\succès en appelant resol () et rejette () sur l'objet différé:
'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');
});
});
Comme quelqu'un l'a mentionné dans une réponse supprimée, success
et error
sont des sucres syntaxiques ajoutés par $http
afin qu'ils ne soient pas là lorsque vous créez votre propre promesse. Vous avez deux options:
$httpBackend
pour définir les attentes et viderL'idée est de laisser votre myService
agir comme il le ferait normalement sans savoir que le test est en cours. $httpBackend
vous permettra de définir les attentes et les réponses et de les vider afin de pouvoir effectuer vos tests de manière synchrone. $http
ne sera pas plus sage et la promesse qu’elle renverra ressemblera et fonctionnera comme une vraie. Cette option est utile si vous avez des tests simples avec peu d'attentes 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');
});
});
});
Si la chose que vous testez a des dépendances compliquées et que toute la configuration est un casse-tête, vous pouvez toujours vouloir vous moquer des services et des appels eux-mêmes comme vous l'avez tenté. La différence est que vous voudrez faire une fausse promesse. L'inconvénient de ceci peut être de créer toutes les fausses promesses possibles, mais vous pouvez le faciliter en créant votre propre fonction pour créer ces objets.
Cela fonctionne parce que nous prétendons résoudre le problème en appelant immédiatement les gestionnaires fournis par success
, error
ou then
, ce qui entraîne son achèvement de manière synchrone.
'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");
});
});
});
Je choisis rarement l'option 2, même dans les grandes applications.
Pour ce que ça vaut, vos gestionnaires loadData
et loadData2
http ont une erreur. Ils référencent response.data
mais les handlers seront appelés avec les données de réponse analysées directement, pas avec l'objet de réponse (il devrait donc être data
au lieu de response.data
).
Utiliser $httpBackend
dans un contrôleur est une mauvaise idée car vous mélangez les problèmes dans votre test. Que vous récupériez des données d'un point de terminaison ou non n'est pas une préoccupation du contrôleur, cela concerne le DataService que vous appelez.
Vous pouvez le voir plus clairement si vous modifiez l'URL du point de terminaison dans le service, vous devrez alors modifier les deux tests: le test de service et le test de contrôleur.
De plus, comme mentionné précédemment, l'utilisation de success
et error
est un sucre syntaxique et nous devrions nous en tenir à l'utilisation de then
et catch
. Mais en réalité, vous pouvez avoir besoin de tester du code "hérité". Donc pour cela j'utilise cette fonction:
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;
}
En appelant cette fonction, vous obtiendrez une véritable promesse qui répondra aux méthodes then
et catch
lorsque vous en aurez besoin et fonctionnera également pour les rappels success
ou error
. Notez que le succès et l'erreur renvoient une promesse elle-même afin qu'elle fonctionne avec les méthodes chaînées then
.
Oui, n'utilisez pas $ httpbackend dans votre contrôleur, car nous n'avons pas besoin de faire de véritables requêtes, vous devez simplement vous assurer qu'une unité effectue son travail exactement comme prévu. Jetez un coup d'œil à ces tests simples, comprendre
/**
* @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
});
});
});
}());