Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sinon - when to use spies/mocks/stubs or just plain assertions?

I'm trying to understand how Sinon is used properly in a node project. I have gone through examples, and the docs, but I'm still not getting it. I have setup a directory with the following structure to try and work through the various Sinon features and understand where they fit in

|--lib
   |--index.js
|--test
   |--test.js

index.js is

var myFuncs = {};

myFuncs.func1 = function () {
   myFuncs.func2();
   return 200;
};

myFuncs.func2 = function(data) {
};

module.exports = myFuncs;

test.js begins with the following

var assert = require('assert');
var sinon = require('sinon');
var myFuncs = require('../lib/index.js');

var spyFunc1 = sinon.spy(myFuncs.func1);
var spyFunc2 = sinon.spy(myFuncs.func2);

Admittedly this is very contrived, but as it stands I would want to test that any call to func1 causes func2 to be called, so I'd use

describe('Function 2', function(){
   it('should be called by Function 1', function(){
      myFuncs.func1();
      assert(spyFunc2.calledOnce);
   });
});

I would also want to test that func1 will return 200 so I could use

describe('Function 1', function(){
   it('should return 200', function(){
      assert.equal(myFuncs.func1(), 200);
   });
});

but I have also seen examples where stubs are used in this sort of instance, such as

describe('Function 1', function(){
   it('should return 200', function(){
      var test = sinon.stub().returns(200);
      assert.equal(myFuncs.func1(test), 200);
   });
});

How are these different? What does the stub give that a simple assertion test doesn't?

What I am having the most trouble getting my head around is how these simple testing approaches would evolve once my program gets more complex. Say I start using mysql and add a new function

myFuncs.func3 = function(data, callback) {
   connection.query('SELECT name FROM users WHERE name IN (?)', [data], function(err, rows) {
          if (err) throw err;
          names = _.pluck(rows, 'name');
          return callback(null, names);
       });
    };

I know when it comes to databases some advise having a test db for this purpose, but my end-goal might be a db with many tables, and it could be messy to duplicate this for testing. I have seen references to mocking a db with sinon, and tried following this answer but I can't figure out what's the best approach.

like image 609
Philip O'Brien Avatar asked Dec 19 '22 06:12

Philip O'Brien


1 Answers

You have asked so many different questions on one post... I will try to sort out.

  1. Testing myFuncs with two functions.

Sinon is a mocking library with wide features. "Mocking" means you are supposed to replace some part of what is going to be tested with mocks or stubs. There is a good article among Sinon documentation which describes the difference well. When you created a spy in this case...

var spyFunc1 = sinon.spy(myFuncs.func1);
var spyFunc2 = sinon.spy(myFuncs.func2);

...you've just created a watcher. myFuncs.func1 and myFuncs.func2 will be substituted with a spy-function, but it will be used to record the calling arguments and call real function after that. This is a possible scenario, but mind that all possibly complicated logic of myFuncs.func1/func2 will run after being called in the test (ex.: database query).

2.1. The describe('Function 1', ...) test suite looks too contrived to me.

It is not obvious which question you actually mean. A function that returns a constant value is not a real life example.. In most cases there will be some parameters and the function under test will implement some algorithm of transmuting the input arguments. So in your test you're going to implement the same algorithm partly to check that the function works correctly. That is where TDD comes in place, which actually supposes you start implementation from a test and take parts of unit test code to implement the method being tested.

2.2. Stub. The second version of unit test looks useless in the given example. func1 is not accepting any parameter.

var test = sinon.stub().returns(200);
assert.equal(myFuncs.func1(test), 200);

Even if you replace the return part with 100 the test will run successfully. The one that makes sense is, for example, replacing func2 with a stub to avoid heavy calculation/remote request (DB query, http or other API request) being launched in a test.

myFuncs.func2 = sinon.spy();
assert.equal(myFuncs.func1(test), 200);
assert(myFuncs.func2.calledOnce);

The basic rule for unit testing is that unit test should be kept as simple as possible, providing check for as smallest possible fragment of code. In this test func1 is being tested so we can neglect the logic of func2. Which should be tested in another unit test. Mind that doing the following attempt is useless:

myFuncs.func1 = sinon.stub().returns(200);
assert.equal(myFuncs.func1(test), 200);

Because in this case you masked the real func1 logic with a stub and you're actually testing sinon.stub().return(). Believe me it works well! :D

  1. Mocking database queries. Mocking a database has always been a hurdle. I could provide some advises.

3.1. Have well fragmented environment. Even for a small project there better exist a development, stage and production completely independent environments. Including databases. Which means you have an automated way of DB creation: scripts or ORM. In this scenario you will be easily maintaining test DB in your test engine using before()/beforeEach() to have a clean structure for your tests.

3.2. Have well fragmented code. There'd better exist several layers. The lowest (DAL) should be separated from business logic. In this case you will write code for business class, simply mocking DAL. To test DAL you can have the approach you mentioned (sinon.mock the whole module) or some specific libraries (ex.: replace db engine with SQLite for tests as described here)

  1. Conclusion. "how these simple testing approaches would evolve once my program gets more complex".

It is hard to maintain unit tests unless you develop your application with tests in mind and, thus, well fragmented. Stick to the main rule - keep each unit test as small as possible. Otherwise you're right, it will eventually get messy. Because the constantly evolving logic of your application will be involved in your test code.

like image 103
Kirill Slatin Avatar answered Mar 26 '23 20:03

Kirill Slatin