Joel's Thoughts

Mocking AngularJs Resource Using SinonJs

November 06, 2016

On my Ecommerce MEAN project, I have this AngularJs controller that calls a Catalog service which in turn returns a $resource promise object:


'use strict';

angular.module('pinkShop')
  .controller('CategoryListCtrl', function ($scope, Catalog, $TreeDnDConvert) {
    $scope.categories = {};  

    $scope.loadTable = function () {
      return Catalog.query().$promise.then(function (categories) {
        //display the returned data
        $scope.categories = $TreeDnDConvert.line2tree(categories, '_id', 'parent');
        return categories;
      });
    };

});

Inside loadTable function, Catalog.query() asynchronously calls a remote API and returns a $promise object. Once $promise is fulfilled, a JSON data will be available for the controller’s consumption and to be assigned to the $scope.categories.

JSON data structure once async call is completed:

[
      { '_id': '580c6648d8d62233f4d92db4', 'name': 'All', '__v': 4, 'slug': 'all', 'children': ['580c6648d8d62233f4d92db5', '580c6648d8d62233f4d92db6', '580c6648d8d62233f4d92db7', '580c6648d8d62233f4d92db8'], 'ancestors': [] },
      { '_id': '580c6648d8d62233f4d92db5', 'name': 'Home', 'parent': '580c6648d8d62233f4d92db4', '__v': 0, 'slug': 'home', 'children': [], 'ancestors': ['580c6648d8d62233f4d92db4'] },
      { '_id': '580c6648d8d62233f4d92db8', 'name': 'Hunting', 'parent': '580c6648d8d62233f4d92db4', '__v': 0, 'slug': 'hunting', 'children': [], 'ancestors': ['580c6648d8d62233f4d92db4'] }
    ]

Test Requirement

I want to write a unit test that will do some assertions when loadTable function is called. The test should verify that $scope.categories is populated with JSON data. Also the test should check if $TreeDnDConvert.line2tree is called at least once.

Issue

Since loadTable is returning an asynchronous call, this can be a little bit tricky to unit test.

Solution

The solution is to mock Catalog service on my unit test environment. Also the mocked service should mimick the real async call.

For unit tests, I used $q for running asynchronous calls and SinonJs for mocking the Catalog service.

The Implementation

I start with the unit test code below:


describe('Controller: CategoryCtrl', function () {

    var CategoryListCtrl, scope, deferred, TreeDnDConvert, returnedResource,
        categories = [
        { '_id': '580c6648d8d62233f4d92db4', 'name': 'All', '__v': 4, 'slug': 'all', 'children': ['580c6648d8d62233f4d92db5', '580c6648d8d62233f4d92db6', '580c6648d8d62233f4d92db7', '580c6648d8d62233f4d92db8'], 'ancestors': [] },
        { '_id': '580c6648d8d62233f4d92db5', 'name': 'Home', 'parent': '580c6648d8d62233f4d92db4', '__v': 0, 'slug': 'home', 'children': [], 'ancestors': ['580c6648d8d62233f4d92db4'] },
        { '_id': '580c6648d8d62233f4d92db8', 'name': 'Hunting', 'parent': '580c6648d8d62233f4d92db4', '__v': 0, 'slug': 'hunting', 'children': [], 'ancestors': ['580c6648d8d62233f4d92db4'] }
        ];

    // load the controller's module
    beforeEach(module('pinkShop'));

    beforeEach(inject(function ($rootScope, _$q_) {
        scope = $rootScope.$new();

        deferred = _$q_.defer();

        returnedResource = {
          $promise: deferred.promise
        };
    }));

 });

As you see, I’m loading $q on the beginning of the test and assigned it’s defer() function result to a global variable ( deferred ). Next is to attached the deferred.promise object inside returnedResource $promise property. This will be used later to mimic the Catalog.query() result.

Next is to write the actual controller’s unit test:


describe('Controller: CategoryCtrl', function () {

    ......more code before here

    describe('CategoryListCtrl', function () {

        beforeEach(inject(function ($controller, _$TreeDnDConvert_) {
            //loading the controller's actual dependency service
            TreeDnDConvert = _$TreeDnDConvert_;

            //mocking the Catalog service
            var mockedCatalog = sinon.stub({
                query: function () { }
            }); 

            //start spying on TreeDnDConvert service 
            sinon.spy(TreeDnDConvert, 'line2tree');           

        }));

        //some clean-up process after every test invocation
        afterEach(function () {
            TreeDnDConvert.line2tree.restore();
            scope.categories = {};
        });

    });

});

Now the fun begins! If you notice, I’m loading the actual $TreeDnDConvert service and start spying on it using sinon.spy. On the other hand, I’m not loading the real Catalog service but instead mocking it using sinon.stub. Also, I’m assigning a query function to the mocked version so it’ll look like Catalog.query().

Remember that calling Catalog.query() will return a $promise object, right? Next is to recreate that same behavior on my test.


describe('Controller: CategoryCtrl', function () {

    ......more code before here

    describe('CategoryListCtrl', function () {

        beforeEach(inject(function ($controller, _$TreeDnDConvert_) {

            ... more code before here

            //create controller
            CategoryListCtrl = $controller('CategoryListCtrl', {
                $scope: scope,
                Catalog: mockedCatalog, //inject the mocked service
                $TreeDnDConvert: TreeDnDConvert
            });

            //set the mocked Catalog service to return a promise
            mockedCatalog.query.returns(returnedResource);

            //call the function under test
            scope.loadTable();

            //resolve the mock Catalog service query call with hard coded data
            deferred.resolve(categories);

            //manually call $digest
            scope.$digest();
        }));

        //some clean-up process after every test invocation
        afterEach(function () {
            TreeDnDConvert.line2tree.restore();
            scope.categories = {};
        });

        //test assertions will be here

    });

});

This is the interesting part! Let me break it down:

  1. First I recreated the CategoryListCtrl using AngularJs $controller. Also, it’s important to mention that I’m injecting the mockedCatalog to the controller’s dependency instead of the real Catalog service.

  2. Next is to set the mockedCatalog service to return the $promise I created earlier. Checkout returnedResource from the beginning.

  3. Call the actual loadTable function to fill the data on the scope. Going back to the actual controller, I’m expecting that calling loadTable will assign returned JSON data to the $scope.categories.

  4. Remember I’m mimicking an async call here ( Catalog.query().$promise ) however this won’t get resolved till I manually call deferred.resolve(). To return the hard coded test data, I need to call deferred.resolve(categories). The variable categories represents the actual JSON data returned from the API call.

  5. Lastly, call AngularJs $digest to kick off the watchers on the scope thus detecting any changes to the scope.categories. Therefore, if my unit test asserts that scope.categories has data at this point, I can conclude that the controller is behaving as expected and test should pass.

With that in mind, I can now write my test assertions. Awesome!


    it('should call TreeDnDConvert.line2tree', function () {
      assert(TreeDnDConvert.line2tree.calledOnce);
    });

    it('should get all the categories', function () {
      expect(angular.equals({}, scope.categories)).to.equal(false);
      expect(scope.categories[0].name.toLowerCase()).to.equal('all');
    });

The Final Unit Test Code

'use strict';

describe('Controller: CategoryCtrl', function () {

    var CategoryListCtrl, scope, deferred, TreeDnDConvert, returnedResource,
        categories = [
        { '_id': '580c6648d8d62233f4d92db4', 'name': 'All', '__v': 4, 'slug': 'all', 'children': ['580c6648d8d62233f4d92db5', '580c6648d8d62233f4d92db6', '580c6648d8d62233f4d92db7', '580c6648d8d62233f4d92db8'], 'ancestors': [] },
        { '_id': '580c6648d8d62233f4d92db5', 'name': 'Home', 'parent': '580c6648d8d62233f4d92db4', '__v': 0, 'slug': 'home', 'children': [], 'ancestors': ['580c6648d8d62233f4d92db4'] },
        { '_id': '580c6648d8d62233f4d92db8', 'name': 'Hunting', 'parent': '580c6648d8d62233f4d92db4', '__v': 0, 'slug': 'hunting', 'children': [], 'ancestors': ['580c6648d8d62233f4d92db4'] }
        ];

    // load the controller's module
    beforeEach(module('pinkShop'));

    beforeEach(inject(function ($rootScope, _$q_) {
        scope = $rootScope.$new();

        deferred = _$q_.defer();

        returnedResource = {
          $promise: deferred.promise
        };
    }));

    describe('CategoryListCtrl', function () {

        beforeEach(inject(function ($controller, _$TreeDnDConvert_) {
            //loading the controller's actual dependency service
            TreeDnDConvert = _$TreeDnDConvert_;

            //mocking the Catalog service
            var mockedCatalog = sinon.stub({
                query: function () { }
            }); 

            //start spying on TreeDnDConvert service 
            sinon.spy(TreeDnDConvert, 'line2tree');

            //create controller
            CategoryListCtrl = $controller('CategoryListCtrl', {
                $scope: scope,
                Catalog: mockedCatalog, //inject the mocked service
                $TreeDnDConvert: TreeDnDConvert
            });

            //set the mocked Catalog service to return a promise
            mockedCatalog.query.returns(returnedResource);

            //call the function under test
            scope.loadTable();

            //resolve the mock Catalog service query call with hard coded data
            deferred.resolve(categories);

            //manually call $digest
            scope.$digest();           

        }));

        //some clean-up process after every test invocation
        afterEach(function () {
            TreeDnDConvert.line2tree.restore();
            scope.categories = {};
        });

        it('should call TreeDnDConvert.line2tree', function () {
            assert(TreeDnDConvert.line2tree.calledOnce);
        });

        it('should get all the categories', function () {
            expect(angular.equals({}, scope.categories)).to.equal(false);
            expect(scope.categories[0].name.toLowerCase()).to.equal('all');
        });

    });

 });

Happy unit testing! :)









  • About
  • Search
  • Resume
  • Powered by Jekyll using the Trio theme