javascriptnode.jsunit-testingsinonproxyquire

How to assert stubbed fetch more than once


Using proxyquire, sinon, and mocha.

I am able to stub fetch on the first call of fetch. But on the second fetch call, which is recursive, I am not able to assert it. From the output, it looks like the assertion may run before the test finishes. You will see this with second fetch console out after assertion.

index.js

var fetch = require('node-fetch');

function a() {
  console.log('function a runs');  
  fetch('https://www.google.com')
    .then((e) => {
      console.log('first fetch');
      b();
    })
    .catch((e)=> {
      console.log('error')
    });
}

function b() {
  fetch('https://www.google.com')
    .then((e) => {
      console.log('second fetch');
    })
    .catch((e)=> {
      console.log('error')
    });
}
a()

test:

describe('fetch test demo', ()=> {

  it('fetch should of called twice', (done)=> {

    fetchStub = sinon.stub();
    fetchStub2 = sinon.stub();
    fetch = sinon.stub();


    fetchStub.returns(Promise.resolve('hello'));
    fetchStub2.returns(Promise.resolve('hi'));

    var promises = [ fetchStub, fetchStub2 ]

    fetch.returns(Promise.all(promises));

    proxy('../index', {
        'node-fetch': fetch
      });

    fetch.should.have.been.callCount(2);
    done()
  });

});

   fetch test demo
function a runs
    1) fetch should of called twice
first fetch
second fetch

  lifx alert test
    - fetch should of called three times
    when rain change is over 50%
      - should run fetch twice


  0 passing (78ms)
  2 pending
  1 failing

  1) fetch test demo fetch should of called twice:
     expected stub to have been called exactly twice, but it was called once
    stub(https://www.google.com) => [Promise] {  } at a (/home/one/github/lifx-weather/foobar.js:5:3)
  AssertionError: expected stub to have been called exactly twice, but it was called once
      stub(https://www.google.com) => [Promise] {  } at a (foobar.js:5:3)
      at Context.it (test/bar.js:22:28)

Solution

  • Updated version

    @dman, since you updated your test case I owe you an updated answer. Although rephrased, the scenario is still unorthodox - it seems like you want to ignore in a sense the 'law of gravity' even though you know it's right there in front of you.

    I'll try to be as descriptive as possible. You have two functions which are doing async stuff by design. a() calls b() sequentially - by the way this is not recursion. Both functions do not notify their callers upon completion / failure, i.e. they are treated as fire-and-forget.

    Now, let's have a look at your test scenario. You create 3 stubs. Two of them resolve to a string and one combining their execution using Promise.all(). Next, you proxy the 'node-fetch' module

    proxy('./updated', {
        'node-fetch': fetch
    });
    

    using the stub that returns the combined execution of stubs 1 & 2. Now, if you print out the resolved value of fetch in either function, you will see that instead of a string it's an array of stubs.

    function a () {
        console.log('function a runs');
        fetch('http://localhost')
            .then((e) => {
                console.log('first fetch', e);
                b();
            })
            .catch((e) => {
                console.log('error');
            });
    }
    

    Which I guess is not the intended output. But let's move over as this is not killing your test anyway. Next, you have added the assertion together with the done() statement.

    fetch.should.have.been.callCount(2);
    done();
    

    The issue here is that whether you are using done() or not, the effect would be exactly the same. You are executing your scenario in sync mode. Of course in this case, the assertion will always fail. But the important thing here is to understand why.

    So, let's rewrite your scenario to mimic the async nature of the behavior you want to validate.

    'use strict';
    
    const chai = require('chai');
    const sinon = require('sinon');
    const SinonChai = require('sinon-chai');
    chai.use(SinonChai);
    chai.should();
    
    const proxy = require('proxyquire');
    
    describe('fetch test demo', () => {
    
        it('fetch should of called twice', (done) => {
    
            var fetchStub = sinon.stub();
            var fetchStub2 = sinon.stub();
            var fetch = sinon.stub();
    
            fetchStub.returns(Promise.resolve('hello'));
            fetchStub2.returns(Promise.resolve('hi'));
    
            var promises = [fetchStub, fetchStub2];
    
            fetch.returns(Promise.all(promises));
    
            proxy('./updated', {
                'node-fetch': fetch
            });
    
            setTimeout(() => {
                fetch.should.have.been.callCount(2);
                done();
            }, 10);
    
        });
    
    });
    

    As you can see, the only change made was wrapping the assertion within a timer block. Nothing much - just wait for 10ms and then assert. Now the test passes as expected. Why?

    Well, to me it's pretty straightforward. You want to test 2 sequentially executed async functions and still run your assertions in sync mode. That sounds cool, but it's not gonna happen :) So you have 2 options:

    Reply based on original test scenario

    It can be done. I've re-factored your provided files a bit so that can be executed.

    index.js

    const fetch = require('node-fetch');
    const sendAlert = require('./alerts').sendAlert;
    
    module.exports.init = function () {
      return new Promise((resolve, reject) => {
    
          fetch('https://localhost')
              .then(function () {
                  sendAlert().then(() => {
                      resolve();
                  }).catch(
                      e => reject(e)
                  );
              })
              .catch(e => {
                  reject(e);
              });
    
      });
    };
    

    alerts.js

    const fetch = require('node-fetch');
    
    module.exports.sendAlert = function () {
      return new Promise((resolve, reject) => {
    
          fetch('https://localhost')
              .then(function () {
                  resolve();
              }).catch((e) => {
              reject(e);
          });
    
      });
    };
    

    test.js

    'use strict';
    
    const chai = require('chai');
    const sinon = require('sinon');
    const SinonChai = require('sinon-chai');
    chai.use(SinonChai);
    chai.should();
    
    const proxy = require('proxyquire');
    
    describe.only('lifx alert test', () => {
    
      it('fetch should of called twice', (done) => {
    
          var body = {
              'hourly': {
                  data: [{
                      time: 1493413200,
                      icon: 'clear-day',
                      precipIntensity: 0,
                      precipProbability: 0,
                      ozone: 297.17
                  }]
              }
          };
    
          var response = {
              json: () => {
                  return body;
              }
          };
    
          const fetchStub = sinon.stub();
    
          fetchStub.returns(Promise.resolve(response));
          fetchStub['@global'] = true;
    
          var stubs = {
              'node-fetch': fetchStub
          };
    
          const p1 = proxy('./index', stubs);
    
          p1.init().then(() => {
    
              try {
                  fetchStub.should.have.been.calledTwice;
                  done();
              } catch (e) {
                  done(e);
              }
          }).catch((e) => done(e));
    
      });
    
    });
    

    What you're trying to do though is a bit unorthodox when it comes to good unit testing practices. Although proxyquire supports this mode of stubbing through a feature called global overrides, it is explained here why should anyone think twice before going down this path.

    In order to make your example pass the test, you just need to add an extra attribute to the Sinon stub called @global and set it to true. This flag overrides the require() caching mechanism and uses the provided stub no matter which module is called from.

    So, although what you're asking can be done I will have to agree with the users that commented your question, that this should not be adopted as a proper way of structuring your tests.