Ich versuche, den besten Weg zu ermitteln, wie Erfolg und Fehlerrückrufe von Controllern in Einzeltests ausgeführt werden können. Ich kann Service-Methoden simulieren, solange der Controller nur die Standardfunktionen von $ q wie 'then' verwendet (siehe Beispiel unten). Ich habe ein Problem, wenn der Controller auf ein Versprechen "Erfolg" oder "Fehler" reagiert. (Sorry, wenn meine Terminologie nicht korrekt ist).
Hier ist ein Beispiel für einen Controller\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);
}
}
]);
Ich habe den folgenden Test
'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);
});
});
Der erste Test ist erfolgreich, und der zweite schlägt fehl mit dem Fehler "TypeError: Object unterstützt die Eigenschaft oder Methode 'success'" nicht. Ich bekomme das in diesem Fall, dass getDeferred.promise Keine Erfolgsfunktion hat. Okay, hier ist die Frage, wie kann man diesen Test auf nette Art und Weise schreiben, damit ich die Bedingungen für "Erfolg", "Fehler" und "Dann" eines verspotteten Dienstes testen kann?
Ich fange an zu denken, dass ich die Verwendung von success () und error () in meinen Controllern vermeiden sollte ...
EDIT
Nachdem ich nun mehr darüber nachgedacht habe und dank der ausführlichen Antwort unten, habe ich ich bin zu dem Schluss gekommen, dass die Handhabung der Rückruf- und Fehlerrückrufe im Controller schlecht ist. Wie HackedByChinese unten den \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ' ist ein syntaktischer Zucker, der von $ http hinzugefügt wird. Wenn ich also versuche, mit dem \\ \\\\\\\\\\\\\\\\ Sorgen in meinen Controller einzugreifen, lasse ich die http-Aufrufe in einen Dienst ein. Der Ansatz, den ich verfolgen werde, ist, den Controller so zu ändern, dass er \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ "erscheinende Fehler nicht verwendet
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';
});
};
}]);
Auf diese Weise kann ich die Fehler-\Erfolgsbedingungen testen, indem ich resol () und reject () für das zurückgestellte Objekt aufrufe:
'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');
});
});
Wie jemand in einer gelöschten Antwort erwähnt hatte, sind success
und error
syntaktischer Zucker, der von $http
hinzugefügt wird, sodass sie nicht vorhanden sind, wenn Sie Ihr eigenes Versprechen erstellen. Sie haben zwei Möglichkeiten:
$httpBackend
, um die Erwartungen und die Spülung festzulegenDie Idee ist, dass Ihr myService
sich so verhält, wie es normalerweise wäre, ohne zu wissen, dass es getestet wird. Mit $httpBackend
können Sie Erwartungen und Antworten festlegen und löschen, sodass Sie Ihre Tests synchron abschließen können. $http
wird nicht klüger sein und das Versprechen, das es zurückgibt, wird wie ein echtes aussehen und funktionieren. Diese Option ist gut, wenn Sie einfache Tests mit wenigen HTTP-Erwartungen haben.
'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');
});
});
});
Wenn das, was Sie testen, komplizierte Abhängigkeiten aufweist und alle Einstellungen Kopfschmerzen verursachen, möchten Sie möglicherweise die Dienste und die Anrufe selbst verspotten, wie Sie es versucht haben. Der Unterschied ist, dass Sie das Versprechen vollständig verspotten möchten. Der Nachteil dabei kann die Erstellung aller möglichen Versprechungen sein. Sie können dies jedoch vereinfachen, indem Sie eine eigene Funktion zum Erstellen dieser Objekte erstellen.
Der Grund, warum dies funktioniert, liegt darin, dass wir vorgeben, dass es durch Aufrufen der von success
, error
oder then
bereitgestellten Handler sofort aufgelöst wird, wodurch es synchron abgeschlossen wird.
'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");
});
});
});
Ich wähle selten Option 2, selbst in großen Anwendungen.
Ihre loadData
- und loadData2
http-Handler haben einen Fehler. Sie verweisen auf response.data
, aber die handlers werden mit den analysierten Antwortdaten direkt aufgerufen, nicht mit dem Antwortobjekt (es sollte also data
anstelle von response.data
sein).
Die Verwendung von $httpBackend
in einem Controller ist eine schlechte Idee, da Sie Bedenken in Ihrem Test vermischen. Ob Sie Daten von einem Endpunkt abrufen oder nicht, ist kein Problem des Controllers. Dies ist ein Problem des DataService, den Sie anrufen.
Sie können dies klarer sehen, wenn Sie die Endpunkt-URL im Service ändern. Sie müssen dann beide Tests ändern: den Service-Test und den Controller-Test.
Wie bereits erwähnt, ist die Verwendung von success
und error
syntaktischer Zucker, und wir sollten uns auf then
und catch
beschränken. In der Realität kann es jedoch sein, dass Sie "alten Code" testen müssen. Dafür verwende ich diese Funktion:
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;
}
Wenn Sie diese Funktion aufrufen, erhalten Sie ein echtes Versprechen, das bei Bedarf auf then
- und catch
-Methoden reagiert und auch für die success
- oder error
-Callbacks funktioniert. Beachten Sie, dass Erfolg und Fehler ein Versprechen selbst zurückgeben, sodass es mit verketteten then
-Methoden funktioniert.
Ja, verwenden Sie nicht $ httpbackend in Ihrem Controller, da wir keine echten Anfragen machen müssen. Sie müssen lediglich sicherstellen, dass eine Einheit ihre Aufgabe genau wie erwartet erfüllt verstehen
/**
* @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
});
});
});
}());