├── .gitignore ├── lib ├── moduleC.js ├── moduleB.js ├── moduleA.js ├── user_service.js └── authentication_service.js ├── .editorconfig ├── package.json ├── test ├── stub │ ├── moduleB.js │ └── authentication_service.js └── spy │ └── moduleA.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /lib/moduleC.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | format: function (name) { 3 | return name.toLowerCase(); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/moduleB.js: -------------------------------------------------------------------------------- 1 | var moduleC = require('./moduleC'); 2 | 3 | module.exports = { 4 | greet: function (name) { 5 | var formattedName = moduleC.format(name); 6 | return 'Hello ' + formattedName; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/moduleA.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // var moduleB = require('./moduleB'); 4 | 5 | module.exports = { 6 | greet: function(name, logger) { 7 | logger.log('Greeting: ' + name); 8 | return 'Hello ' + name; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /lib/user_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | findById: function (id, cb) { 5 | // simulate async call to DB with 1 user 6 | process.nextTick(function () { 7 | if (id !== 123) { 8 | var error = new Error('User not found'); 9 | return cb(error); 10 | } else { 11 | return cb(null, {id: 123, name: 'Obi-wan'}); 12 | } 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sinonjs-examples", 3 | "version": "0.0.1", 4 | "description": "Some examples on using sinonjs and understanding its api.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/**/*.js" 8 | }, 9 | "author": "Olivier Nguyen", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "chai": "^2.2.0", 13 | "mocha": "^2.2.4", 14 | "sinon": "^1.14.1", 15 | "sinon-chai": "^2.7.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/authentication_service.js: -------------------------------------------------------------------------------- 1 | var userService = require('./user_service'); 2 | 3 | module.exports = { 4 | login: function(req, res) { 5 | userService.findById(req.body.userId, function (error, user) { 6 | if (error) { 7 | return res.send(error.message); 8 | } 9 | 10 | return res.send(user); 11 | }); 12 | }, 13 | 14 | loginWithCallback: function (req, res, cb) { 15 | userService.findById(req.body.userId, function (error, user) { 16 | if (error) { 17 | return cb(error); 18 | } 19 | 20 | return cb(null, user); 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /test/stub/moduleB.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var moduleB = require('../../lib/moduleB'); 4 | var moduleC = require('../../lib/moduleC'); 5 | 6 | var chai = require('chai'); 7 | var expect = chai.expect; 8 | var sinon = require('sinon'); 9 | var sinonChai = require('sinon-chai'); 10 | chai.use(sinonChai); 11 | 12 | describe('moduleB', function () { 13 | describe('greet', function () { 14 | it('1st example using a stub', function () { 15 | var stub = sinon.stub(moduleC, 'format'); 16 | stub.returns('Eric') 17 | 18 | var name = 'JaMes ARMStrOng'; 19 | var greetings = moduleB.greet(name); 20 | 21 | expect(moduleC.format).to.have.been.calledOnce; 22 | expect(moduleC.format).to.have.been.calledWith(name); 23 | 24 | expect(greetings).to.equal('Hello Eric'); 25 | 26 | stub.restore(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/spy/moduleA.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var moduleA = require('../../lib/moduleA'); 4 | 5 | var chai = require('chai'); 6 | var expect = chai.expect; 7 | var sinon = require('sinon'); 8 | var sinonChai = require('sinon-chai'); 9 | chai.use(sinonChai); 10 | 11 | describe('moduleA', function () { 12 | 13 | describe('greet', function () { 14 | var logger; 15 | 16 | it('1st example using a spy', function () { 17 | logger = { 18 | log: sinon.spy() 19 | }; 20 | 21 | var greetings = moduleA.greet('James', logger); 22 | 23 | expect(logger.log).to.have.been.calledOnce; 24 | expect(logger.log).to.have.been.calledWith('Greeting: James'); 25 | 26 | expect(greetings).to.equal('Hello James'); 27 | 28 | logger.log.reset(); 29 | }); 30 | 31 | it('2nd example using a spy', function () { 32 | logger = { 33 | log: function (msg) { 34 | console.log(msg); 35 | } 36 | }; 37 | 38 | sinon.spy(logger, 'log'); 39 | 40 | var greetings = moduleA.greet('James', logger); 41 | 42 | expect(logger.log).to.have.been.calledOnce; 43 | expect(logger.log).to.have.been.calledWith('Greeting: James'); 44 | 45 | expect(greetings).to.equal('Hello James'); 46 | 47 | logger.log.restore(); 48 | }); 49 | 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/stub/authentication_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var authenticationService = require('../../lib/authentication_service'); 4 | var userService = require('../../lib/user_service'); 5 | 6 | var chai = require('chai'); 7 | var expect = chai.expect; 8 | var sinon = require('sinon'); 9 | var sinonChai = require('sinon-chai'); 10 | chai.use(sinonChai); 11 | 12 | describe('AuthenticationService', function () { 13 | var req, res, stub; 14 | 15 | req = { 16 | body: { 17 | userId: 123 18 | } 19 | }; 20 | 21 | beforeEach(function () { 22 | res = { 23 | send: sinon.spy() 24 | }; 25 | 26 | stub = sinon.stub(userService, 'findById'); 27 | }); 28 | 29 | afterEach(function () { 30 | res.send.reset(); 31 | stub.restore(); 32 | }); 33 | 34 | describe('login', function () { 35 | it('should return an error message if the authentication fails', function (done) { 36 | // setup: 37 | var error = new Error('Authentication failed.'); 38 | stub.callsArgWithAsync(1, error); 39 | 40 | // when: 41 | authenticationService.login(req, res); 42 | 43 | // then: 44 | process.nextTick(function () { 45 | expect(res.send).to.have.been.calledWith(error.message); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should return the user if the authentication succeeds', function (done) { 51 | // setup: 52 | var userFixture = {id: 123, name: 'Obi one'}; 53 | stub.callsArgWithAsync(1, null, userFixture); 54 | 55 | // when: 56 | authenticationService.login(req, res); 57 | 58 | // then: 59 | process.nextTick(function(){ 60 | expect(res.send).to.have.been.calledWith(userFixture); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('loginWithCallback', function () { 67 | it('should return an error message if the authentication fails', function (done) { 68 | // setup: 69 | var error = new Error('Authentication failed.'); 70 | stub.callsArgWithAsync(1, error); 71 | 72 | // when: 73 | authenticationService.loginWithCallback(req, res, function (err, result) { 74 | expect(err.message).to.equal(error.message); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('should return the user if the authentication succeeds', function (done) { 80 | // setup: 81 | var userFixture = {id: 123, name: 'Obi one'}; 82 | stub.callsArgWithAsync(1, null, userFixture); 83 | 84 | // when: 85 | authenticationService.loginWithCallback(req, res, function (error, user) { 86 | expect(user).to.equal(userFixture); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sinon.JS is a standalone unit testing library for JavaScript. It provides spies, stubs and mocks. We used it extensively while building our services in Node.js but it also supports objects to test your client side code. We found it very useful and easy to use, but its API was not easy to understand at first. In this post, I want to share the lessons learned from using Sinon's spies and stubs. 2 | 3 | Notes: 4 | In this post, we are using mocha (http://mochajs.org/) as the test runner and chaijs (http://chaijs.com/) to express our tests assertions. Sinon has its own assertion API. We are using the sinon-chai module (https://github.com/domenic/sinon-chai) to bridge Sinon' assertions with chaijs' assertions. 5 | 6 | The sample code can be found at: https://github.com/olivierntk/unit-testing-with-sinonjs 7 | 8 | 1. Spies 9 | ==================== 10 | 11 | ###### From SinonJS's docs: 12 | 13 | >"A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. A test spy can be an anonymous function or it can wrap an existing function." 14 | 15 | 16 | 1.1 Using spies as an anonymous function: sinon.spy() 17 | --------------------- 18 | 19 | Let's consider moduleA (under /lib/moduleA.js) and see how we would test it. 20 | In moduleA, the function ```greet()``` takes a name and a logger object that has a ```log()``` method. 21 | The unit test should verify that the result string is correct and that the logger's log method was called with the correct parameter. 22 | In the test below, we constructed a logger object with a log method. Because logger.log is a spy, after we execute the method under test, we can inspect how many times it has been called or which arguments it received. 23 | 24 | it('1st example using a spy', function () { 25 | logger = { 26 | log: sinon.spy() 27 | }; 28 | var greetings = moduleA.greet('James', logger); 29 | 30 | expect(logger.log).to.have.been.calledOnce; 31 | expect(logger.log).to.have.been.calledWith('Greeting: James'); 32 | 33 | // Verify the method returns the expected result 34 | expect(greetings).to.equal('Hello James'); 35 | 36 | // Reset the spy so that this test does not affect other tests. 37 | logger.log.reset(); 38 | }); 39 | 40 | Note that we called reset on the log method. This clears all state on the spy for the next test. 41 | 42 | 1.2 Using spies as a function wrapper: sinon.spy(object, "method"); 43 | --------------------- 44 | 45 | You can also use a spy as a wrapper on an existing function. In the example below, we have an implemention of the log function. 46 | 47 | it('2nd example using a spy', function () { 48 | logger = { 49 | log: function (msg) { 50 | console.log(msg); 51 | } 52 | }; 53 | 54 | // Spying on the log function 55 | sinon.spy(logger, 'log'); 56 | 57 | var greetings = moduleA.greet('James', logger); 58 | 59 | expect(logger.log).to.have.been.calledOnce; 60 | expect(logger.log).to.have.been.calledWith('Greeting: James'); 61 | 62 | expect(greetings).to.equal('Hello James'); 63 | 64 | logger.log.restore(); 65 | }); 66 | 67 | The point to note is that the spy lets the code execute so you will see the log function execute and the message in your console. 68 | The test itself doesn't change except that we are using restore in this scenario. 69 | 70 | 2. Stubs 71 | ==================== 72 | 73 | ###### From the docs: 74 | >"Test stubs are functions (spies) with pre-programmed behavior. They support the full test spy API in addition to methods which can be used to alter the stub’s behavior. 75 | As spies, stubs can be either anonymous, or wrap existing functions. When wrapping an existing function with a stub, the original function is not called." 76 | 77 | So stubs are spies and can be used the same way (as an anonymous function or wrapping an existing one). They differ by their ability to have a specified behavior and they do NOT let the original method execute. 78 | 79 | 2.1 Stubbing a dependency: sinon.stub(object, "method") 80 | --------------------- 81 | 82 | Stubs are particularly useful if you want to isolate the module under test from its dependencies. 83 | 84 | Again, let's look at an example. Our moduleB depends on moduleC. To properly unit test moduleB, we want to isolate it from moduleC's internal working to ensure we are just testing moduleB's code. 85 | 86 | In '/test/sub/moduleB': 87 | 88 | // require the dependencies 89 | var moduleC = require('../../lib/moduleC'); 90 | 91 | it('1st example using a stub', function () { 92 | // stub moduleC's format 93 | var stub = sinon.stub(moduleC, 'format'); 94 | 95 | // specify the behavior of moduleC's format method 96 | stub.returns('Eric') 97 | 98 | var name = 'JaMes ARMStrOng'; 99 | 100 | // execute the method under test 101 | var greetings = moduleB.greet(name); 102 | 103 | // stubs are spies to they have the same recording abilities 104 | expect(moduleC.format).to.have.been.calledOnce; 105 | expect(moduleC.format).to.have.been.calledWith(name); 106 | 107 | // because we defined the stub's behavior, moduleC's format 108 | // method is going to return the string 'Eric' but 109 | // will not execute 110 | expect(greetings).to.equal('Hello Eric'); 111 | 112 | // restore the stub 113 | stub.restore(); 114 | }); 115 | 116 | Here, we used the ability of a stub to have a pre-programmed behavior (using ```.returns()```) so we can control the dependency and since a stub does not let the original function execute, we can properly isolate moduleB's function. 117 | 118 | 2.2 Testing asynchronous code 119 | --------------------- 120 | 121 | When programmers start with Node.js, dealing with asynchronous programming can be confusing and unit testing asynchronous code can be even more so. But once you get a grasp of sinon's api, it can be quite easy. 122 | 123 | Let's look at [authentication\_service.js](https://github.com/olivierntk/unit-testing-with-sinonjs/blob/master/lib/authentication\_service.js). It has a login method which internally uses the asynchronous userService.findById method. findById() takes an id as its first parameter and a function to execute once it receives the data from the userService. The file /test/stub/authentication_service.js shows how we can test it. 124 | 125 | describe('login', function () { 126 | var req, res, stub; 127 | 128 | req = { 129 | body: { 130 | userId: 123 131 | } 132 | }; 133 | 134 | beforeEach(function () { 135 | res = { 136 | send: sinon.spy() 137 | }; 138 | 139 | stub = sinon.stub(userService, 'findById'); 140 | }); 141 | 142 | afterEach(function () { 143 | res.send.reset(); 144 | stub.restore(); 145 | }); 146 | 147 | it('should return an error message if the authentication fails', function () { 148 | // setup: 149 | var error = new Error('Authentication failed.'); 150 | stub.callsArgWithAsync(1, error); 151 | 152 | // when: 153 | authenticationService.login(req, res); 154 | 155 | // then: 156 | process.nextTick(function () { 157 | expect(res.send).to.have.been.calledWith(error.message); 158 | done(); 159 | }); 160 | }); 161 | 162 | it('should return the user if the authentication succeeds', function () { 163 | // setup: 164 | var userFixture = {id: 123, name: 'Obi one'}; 165 | stub.callsArgWithAsync(1, null, userFixture); 166 | 167 | // when: 168 | authenticationService.login(req, res); 169 | 170 | // then: 171 | process.nextTick(function(){ 172 | expect(res.send).to.have.been.calledWith(userFixture); 173 | done(); 174 | }); 175 | }); 176 | }); 177 | 178 | There are quite a few points to talk about in the code above, so let's go slowly. 179 | 180 | #### beforeEach/afterEach 181 | First, note the use of mocha's hooks (beforeEach/afterEach) to create a res.send spy and userService.findById stub before each tests and reset/restore the spy/stub after each tests. 182 | It is important to reset/restore spies and stubs for subsequent tests and mocha's hooks are a convenient place to do this. 183 | 184 | #### callsArg, callsArgWith, yields, callsArgWithAsync, yieldsAsync etc ... 185 | SinonJS has a whole set of APIs around callsArg/yields and their asynchronous versions. Let's take a moment to look at the concepts and you will be able to understand most of Sinon's stub APIs 186 | 187 | ###### From the docs: 188 | >"stub.callsArg(index): Causes the stub to call the argument at the provided index as a callback function. stub.callsArg(0); causes the stub to call the first argument as a callback." 189 | 190 | ```callsArg()``` allows us to execute the callback passed to the stub by passing the index of the callback in the list of parameters received by the stub. This callback in our case holds the logic we're trying to test. 191 | ```callsArgWith()``` takes the callback index and the arguments that should be passed to the callback. ```yield``` is the same as callsArgWith except we don't pass the callback index, as yield just picks up the first callback it finds. 192 | 193 | callsArgWith/yields have an asynchronous counterpart: callsArgWithAsync/yieldsAsync. The difference in the async versions is how the callback gets executed. In our case, the callback is executed asynchronously (by the userService) so we use callsArgWithAsync. 194 | 195 | Since ```userService.findById()``` first takes an id and then a callback, the callback's index is 1. And because we want to test the error case, we pass it an error object. Hence: ```stub.callsArgWithAsync(1, error);```. Using this method, we can control what our callback receives without having to execute the internal code of the userService. If you want to use yields, then you'd write ```stub.yieldsAsync(error);```. 196 | 197 | #### process.nextTick() 198 | After setting up our fixtures and stub, we execute the method under test. The method under test puts our callback with our logic and spies on the event loop. 199 | If we were to do the expectation synchronously, as in: 200 | 201 | it('should return an error message if the authentication fails', function (done) { 202 | // setup: 203 | var error = new Error('Authentication failed.'); 204 | stub.callsArgWithAsync(1, error); 205 | 206 | // when: 207 | authenticationService.login(req, res); 208 | 209 | // then: 210 | expect(res.send).to.have.been.calledWith(error.message); 211 | done(); 212 | }); 213 | 214 | Our tests would fail because we'd check on the spy before it is called in the userService.findById() callback. 215 | To make sure the expectation executed after the spies were called, we put the expectation in a function that we run on the event loop after the callback using ```process.nextTick()```; 216 | We then call ```done()``` to tell mocha that our test completed. 217 | 218 | Notes: 219 | * If you look at the tests examples, I also added the case where userService.findById() returns a user. We are passing null as the first parameter (for the error) and then the 'found' user. 220 | * I also added an example in case the function you are testing takes a callback as one of its parameters. 221 | 222 | Summary 223 | ==================== 224 | I hope you found this article usefull and clarified how to use SinonJS. There are quite a few concepts in this post and it took us some time to understand Sinon's API and how to use it properly. Please comment back with questions or your own lessons learned. Happy unit testing! 225 | --------------------------------------------------------------------------------