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'] }
]
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.
Since loadTable is returning an asynchronous call, this can be a little bit tricky to unit test.
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.
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:
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.
Next is to set the mockedCatalog service to return the $promise I created earlier. Checkout returnedResource from the beginning.
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.
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.
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');
});
'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! :)