Quoc Vu bio photo

Quoc Vu

Son, husband, father, and code addict.

Email LinkedIn Github

In this post, I will show how to stub functions from imported modules so that to isolate the code to be unit tested.

Let’s first write a function to test. This function simply retrieves a list of organizations from GitHub using its REST API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var request = require('request');

module.exports = {
  getOrgs: function() {
    var options = {
      uri: 'https://api.github.com/organizations',
      headers: {
        'User-Agent': 'My-test-app'
      }
    }

    request.get(options, function(err, response, body) {
      if (err) {
        console.error('An error occured');

        // handle this error
      } else {
        if (response.statusCode === 200) {
          console.log('Received', body.length, 'organizations');

          // do something with the orgs in the body
        } else {
          console.warn('Cannot retrieve organizations', response.statusCode);

          // handle this corner case
        }
      }
    });
  }  
}

Through this example, we show how to stub a method (get) from an imported module (request) and how to verify the different parts of the callback function being executed.

Here we rely on the request module (imported by line 1) to make an HTTPS request to GitHub. But, when running unit tests, we cannot let it makes round trips to GitHub and hoping it would always return the results we need. Instead we need to control the results of the request.get() method so that we can achieve the desired test coverage of our code from line 13 to 27. For that we need to stub this method and we will do that with the help of the SinonJS module.

Let’s see what our unit tests look like. We assume the code above is in a file called app.js located in the same directory as this test code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var
  mocha = require('mocha'),
  should = require('should'),
  sinon = require('sinon'),
  request = require('request'),
  app = require('./app');

describe('sinon stub tutorial', function() {
  it('should get some organizations returned', function(doneCallback) {
    sinon.spy(console, 'log');      
    sinon
      .stub(request, 'get')
      .yields(null, { statusCode: 200 }, [{ name: 'org-one' }, { name: 'org-two'}]);

    app.getOrgs();

    request.get.calledOnce.should.be.true();
    console.log.calledOnce.should.be.true();
    console.log.calledWith('Received', 2, 'organizations').should.be.true();

    request.get.restore();    
    console.log.restore();
    doneCallback();
  });

  it('should have a bad status code', function(doneCallback) {
    sinon.spy(console, 'warn');      
    sinon
      .stub(request, 'get')
      .yields(null, { statusCode: 403 }, null);

    app.getOrgs();

    request.get.calledOnce.should.be.true();
    console.warn.calledOnce.should.be.true();

    request.get.restore();    
    console.warn.restore();
    doneCallback();
  });

  it('should have an error', function(doneCallback) {
    sinon.spy(console, 'error');      
    sinon
      .stub(request, 'get')
      .yields('something really bad happened', null, null);

    app.getOrgs();

    request.get.calledOnce.should.be.true();
    console.error.calledOnce.should.be.true();

    request.get.restore();    
    console.error.restore();
    doneCallback();
  });
});

Stubs

Line 5 imports the request module again althought it was already imported in app.js file.
This is necessary to stub its functions later. In Node.js require(..) loads modules once into a cache. Thus we load it here first, stub the functions we need, and then app.js will just use it. Thus the order of the imported modules at lines 5 and 6 is very important.

Now that the request object is accessible in our tests, we can replace its get() function by a stub. Test stubs are functions with pre-programmed behavior allowing us to control the code paths to test. We stub request.get() at lines 12, 29, and 45 with the function sinon.stub(request, 'get'). That creates a stub.

To control the results of the stub, we use its .yields() method at lines 13, 30, and 46. The 3 parameters correspond to the values being passed into the anonymous function at line 12 of app.js. We passed different set of values to cover the different code paths of this anonymous function.

And that’s how simple it is to stub functions of imported modules.

Spies

To verify the various parts of the anonymous function are being executed properly, we use spies. A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. We create a spy using sinon.spy() method as shown at lines 10, 27, and 43. We can check the spies are being called as expected at lines 18, 19, 35, and 51.

Note stubs are also spies, thus we can also check they are being executed as shown at lines 17, 34, and 50.