Recently I've been working with clientside unit testing for an Angular project. The tests I've been creating have been using Jasmine for unit testing and Sinon (http://sinonjs.org) for spies, stubs and mocks, so I thought it might be useful to quickly blog about what I've learnt using Sinon.
As mentioned before, using Sinon gives us the ability to create spies, stubs and mocks.
Spies
Spies allow us to test that the required calls have been made in the method under test. More specifically, when using a spy we can check the arguments, return value and any exceptions thrown from a call.
Stubs
Stubs offer the same functionality as spies, but additionally they allow us to wrap an original function with a stub meaning our test will call the stubbed function and not the original function.
Mocks
Mocks offer the same functionality as stubs and mocks, but additionally we can use pre-programmed expectations. Hence a mock can fail your test if it isn't used as you would expect.
When should I use which?
As you can see, Mocks offer the most functionality (combining elements of spies and stubs with pre-programmed expectations). So, when choosing which to use I always try to use objects or approaches that provide only the functionality I require. As this means there's less scope for unexpected behaviour.
So, (very generally speaking) if you need to test that a function has been called on an object or test it's return value use Spies; if you need to replace an original function with a test function use Stubs; if you need to include pre-programmed expectations in your tests us Mocks.
Code Examples
Now I'm going to run through some very basic code examples to provide an overview of how Sinon spies, stubs and mocks are used in clientside tests. I'm going to test a basic AngularJS controller using Jasmine for the testing framework and run my tests in Visual Studio using Chutzpah via a VS add-in.
To begin with, I've got a very simple controller and dataContext:
Products Controller
(function () {
'use strict';
var controllerId = 'products';
angular.module('app').controller(controllerId, [
'common', 'dataContext',
function (common, dataContext) {
var vm = this;
vm.products = [];
activate();
function activate() {
vm.products = dataContext.getProducts();
common.activateController([], controllerId);
}
}]);
})();
Data Context
(function () {
'use strict';
var serviceId = 'dataContext';
angular.module('app').factory(serviceId, [
function () {
var service = {
getProducts: getProducts,
getOrders: getOrders
};
init();
return service;
function init() {
// Implementation...
}
function getProducts() {
return [
{ id: 1, name: "Product 1" },
{ id: 2, name: "Product 2" },
{ id: 3, name: "Product 3" },
{ id: 4, name: "Product 4" },
{ id: 5, name: "Product 5" }
];
}
function getOrders() {
// Implementation...
}
}]);
})();
Testing with Spies
Now I'm going to test that when the controller is activated that the products
array is populated from the dataContext.getProducts
function.
(function () {
describe("products Controller", function () {
var testContext = {
$controller: null,
stubs: [],
mocks: [],
spies: []
};
beforeEach(function () {
module('app');
inject(function ($controller, dataContext) {
testContext.$controller = $controller;
// Create spy and add to testContext.spies for restoration later.
testContext.spies['getProducts'] = sinon.spy(dataContext, "getProducts");
});
});
afterEach(function () {
// Restore any spies
for (var key in testContext.spies) {
testContext.spies[key].restore();
}
});
it('activation loads products list', inject(function (common, dataContext) {
// Arrange/Act: Create and initialize the controller
var controller = testContext.$controller('products', { common: common, dataContext: dataContext });
// Assert: Products has been populated
expect(controller.products).not.toBeNull();
// Assert: dataContext.getProducts was called
sinon.assert.calledOnce(testContext.spies['getProducts']);
}))
});
})();
In the above test, we create a spy on the getProducts
function of the dataContext
object. Then in the test assertions we use Jasmine to test that the controller.products value is not null. Finally, we use sinon assertions to test that the spy function was called at least once.
Testing Using Stubs
Now suppose our dataContext
makes a call to a REST API or some other component that we don't want to call during our test. So we need to replace the dataContext.getProducts
function with our own implementation for this test.
(function () {
describe("Products Controller", function () {
var testContext = {
$controller: null,
products: [ {id: 1, name: "product 1"} , {id: 2, name: "product 2"} ],
stubs: [],
mocks: [],
spies: []
};
beforeEach(function () {
module('app');
inject(function ($controller, dataContext) {
testContext.$controller = $controller;
// Create stub and add to testContext.stubs for restoration later.
testContext.stubs['getProducts'] = sinon.stub(dataContext, "getProducts", function () {
// Replace the getProducts function with our own implementation.
return testContext.products;
});
});
});
afterEach(function () {
// Restore any stubs
for (var key in testContext.stubs) {
testContext.stubs[key].restore();
}
});
it('activation loads products list', inject(function (common, dataContext) {
// Arrange/Act: Create and initialize the controller
var controller = testContext.$controller('products', { common: common, dataContext: dataContext });
// Assert: Products has been populated
expect(controller.products).not.toBeNull();
// Assert: dataContext.getProducts was called
sinon.assert.calledOnce(testContext.stubs['getProducts']);
// Assert: dataContext.getProducts returned our testContext.products array
expect(controller.products).toEqual(testContext.products);
}))
});
})();
The above spec replaces the dataContext.getProducts
function with our own implementation that simply returns the testContext.products
array. Then in the test we assert that the controller.products
array has been populated and that the dataContext.getProducts
function was called once. In addition, we also check that the dataContext.getProducts
function returns the list of products from testContext.products
.
Test Using Mocks
Now we'd like to completely mock the dataContext
object so we can pre-program expectations of the number of times functions are called and what they return.
(function () {
describe("Products Controller", function () {
var testContext = {
$controller: null,
products: [{ id: 1, name: "product 1" }, { id: 2, name: "product 2" }],
stubs: [],
mocks: [],
spies: []
};
beforeEach(function () {
module('app');
inject(function ($controller, dataContext) {
testContext.$controller = $controller;
// Create stub and add to testContext.stubs for restoration later.
testContext.mocks['dataContext'] = sinon.mock(dataContext);
// Pre-programmed expectation: getProducts is called once and returns our products list.
testContext.mocks['dataContext'].expects("getProducts").once()
.returns(testContext.products);
// Pre-programmed expectation: getOrders is never called.
testContext.mocks['dataContext'].expects("getOrders").never();
});
});
afterEach(function () {
// Restore any stubs
for (var key in testContext.mocks) {
testContext.mocks[key].restore();
}
});
it('activation loads products list', inject(function (common, dataContext) {
// Arrange/Act: Create and initialize the controller
var controller = testContext.$controller('products', { common: common, dataContext: dataContext });
// Assert: Products has been populated
expect(controller.products).not.toBeNull();
// Assert: dataContext.getProducts returned our testContext.products array
expect(controller.products).toEqual(testContext.products);
// Assert: Verify mock pre-programmed expectations
testContext.mocks['dataContext'].verify();
}))
});
})();
In the beforeEach
function we create a mock of the dataContext
object. Now we pre-program the expectations we have, which in this case are that dataContext.getProducts
is called once and returns our testContext.products
array. In addition to this we also pre-program the expectation that the dataContext.getOrders
function is never called.
Then in the test, we assert that the controller.products
array has been populated, that the dataContext.getProducts
function returns the expected array of products and finally the .verify()
call asserts that all of the pre-programmed expectations of our mock have been met.
In conclusion, clientside testing is more available and easier to pick up that ever and I'm really enjoying learning more and more about it.