I have a simple Angular application that lists some object requests from a backend, allows the users to click once to edit the item, and then click again to update the object in the backend.
Here's a Codepen demonstration. I've replaced the backend service in this Codepen to return a $q promises, rather than making actual backend requests.
app.controller("MyCtrl", function($scope, BackendService,
Messages) {
function handleServerError(data) {
$scope.message = Messages.serverError;
}
$scope.objects = [];
BackendService.getAllObjects().then(function(data) {
$scope.objects = data.data;
}, handleServerError);
$scope.edit = function(o) {
o.isBeingEdited = true;
}
$scope.save = function(o) {
// Save
var prom = BackendService.updateObject(o);
prom.then(function(data) {
o.isBeingEdited = false;
}, function(data) {
o.isBeingEdited = false;
handleServerError(data);
});
};
});
app.service("BackendService", function($http, $q) {
this.getAllObjects = function() {
return $http.get("getAllObjects");
};
this.saveNewObject = function(object) {
return $http.post("saveNewObject", object);
};
this.updateObject = function(object) {
return $http.post("updateObject", object);
};
this.deleteObject = function(object) {
return $http.post("deleteObject", object);
};
});
Here is the demonstration of the Jasmine tests I've written for this:
describe("AppController", function() {
beforeEach(module('MyApp'));
var $controller; //controller instantiation function
var myCtrl; //The actual controller object
var $scope; //mocked scope object
var $q, $rootScope;
//External dependencies
var mockMessages, mockService;
beforeEach(inject(function(_$controller_, _$rootScope_, _Messages_, _BackendService_, _$q_){
//Inject angular controller instantiation function
$controller = _$controller_;
$q = _$q_;
$rootScope = _$rootScope_;
//Mocked dependencies (are actually injected as real objects which we'll mock later);
mockMessages = _Messages_;
mockService = _BackendService_;
spyOn(mockService, 'getAllObjects').and.callFake(function(){
return $q.resolve([]);
});
//Set up objects
$scope = $rootScope.$new();
myCtrl = $controller("MyCtrl", {$scope: $scope, Messages: mockMessages, BackendService: mockService});
}));
describe("$scope.edit(o)", function(){
var o;
beforeEach(function(){
//Reset object before each test
o = {
someContent:"foo"
};
});
describe("o.isBeingEdited = false", function(){
it ("sets .isBeingEdited to true", function(){
o.isBeingEdited = false;
$scope.edit(o);
expect(o.isBeingEdited).toBe(true);
});
});
});
describe("$scope.save(o)", function(){
var o;
beforeEach(function(){
o = {
someContent:"foo"
};
o.isBeingEdited = true;
})
it ("calls BackendService.updateObject(o)", function() {
spyOn(mockService, 'updateObject').and.callFake(function() {
return $q.resolve();
});
$scope.save(o);
$scope.$digest();
expect (mockService.updateObject).toHaveBeenCalledWith(o);
});
describe("if BackendService error", function() {
beforeEach(function() {
spyOn(mockService, 'updateObject').and.callFake(function() {
return $q.reject();
});
$scope.save(o);
$scope.$digest();
});
it ("sets .isBeingEdited to false", function() {
expect(o.isBeingEdited).toBe(false);
});
it ("displays error message", function() {
expect ($scope.message).toEqual(mockMessages.serverError);
})
});
describe("if BackendService success", function() {
beforeEach(function(){
spyOn(mockService, 'updateObject').and.callFake(function() {
return $q.resolve();
});
$scope.save(o);
$scope.$digest();
});
it ("sets .isBeingEdited to false", function() {
expect(o.isBeingEdited).toBe(false);
});
});
});
});
Could I have some feedback on the general code structure and the way I'm testing it?
Here's a few things I'm concerned about:
Initialisation method in controller
I call
BackendService.getAllObjects()when the controller is initialised - in order to get the original list of items. Are such initialisation calls good - or is there a more preferable way of doing this?This presented a problem with my tests for example, where I've had to put these lines in:
spyOn(mockService, 'getAllObjects').and.callFake(function(){ return $q.resolve([]); });As it was otherwise wanting to make a call to a real method when it instantiated the controller.
Scope of
handleServerErrorThis is a declared as function in its own scope, rather than as
$scope.handleServerError- what's preferable here?In the tests I inject the actual dependencies objects, then mock their behavior.
The reason I did this was because I didn't want to have create mock objects like:
var fakeBackendService = { updateObject: function(){}, getAllObjects: function(){}, //etc }