2
\$\begingroup\$

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 handleServerError

    This 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
    }
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.