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\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.