How to assert stubbed fetch more than once
Asked Answered
H

3

7

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)
Hystero answered 29/4, 2017 at 1:17 Comment(4)
why not test the files separately and stub the second file?Juxtapose
That is what I am currently doing. It would be nice to do the other though.Hystero
no it wouldn't when you write unit tests you want to test each function/module in isolation without having to think is the unwanted behavior is due to a bug in the required module. however if you want to do it, this is how to do it, in your test proxyquire alert module giving it the fetch mock, then proxyquire the module to test giving it your local alert module as a replacement for the alert that will give it the chainJuxtapose
can you share the whole files to that reproduce this, could it be because you're requiring the module before calling proxy?Dufour
A
3

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:

  • Have your functions notify callers upon completion and then run your assertions in truly async mode
  • Mimic the async nature of things using unorthodox techniques

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.

Aceto answered 5/5, 2017 at 13:26 Comment(5)
+1 From me! Not only for the actual technical implementation, but also for the comment about unorthodox practice. In general it is not a good practice to use knowledge of the internals of any unit/system, except the system under test (sut). In this case the OP is using knowledge of the internals of the alert.js module while testing functionality in index.jsSpencerspencerian
I used ` fetchStub['@global'] = true;` but still no pass. I console.log the second fetch. What I see is the test finishes before second fetch runs/completes. I made sure to use doneHystero
You mean you tried my exact files and didn't work? Adding just the @global attribute will not work. There are other changes, e.g. only the index.js file is proxied, etc. Try again with my version of the test and then adjust your original code accordingly.Aceto
Yours works, but it's not the same as my source code since you are wrapping the fetches in promises. This would be unnecessary to do in my source and I wouldn't want to do it just for the test. I am going to update with a more simplified source code for testing.Hystero
Thank you for your help and explanations!Hystero
H
0

Here is also a alternative way to do this using Promise.all().

Note: this won't work if using fetch's json method and you need to pass data in the resolve() for logic on data. It will only pass in the stubs when resolved. However, it will assert the number of times called.

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

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

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

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

    var promises = [ fetchStub, fetchStub2 ]
    var promise  = Promise.all(promises);

    fetch.returns(promise);

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

    return promise.then(() => {
      fetch.should.have.callCount(2);
    });

  });

});
Hystero answered 7/5, 2017 at 4:53 Comment(0)
C
0

I have found another way to get things done. May be this could work for someone.

    describe('Parent', () => {
      let array: any = [];
      before(async () => {
        array = await someAsyncDataFetchFunction();
        asyncTests();
      });
      it('Dummy test to run before()',async () => {
        expect(0).to.equal(0); // You can use this test to getting confirm whether data fetch is completed or not.
      });
      function asyncTests() {
        array.forEach((currentValue: any) => {
            describe('Child', async () => {
                it('Test '+ currentValue ,() => {
                    expect(currentValue).to.equal(true);
                  })
              })
          });
       }
   });

That's how I achieved the assertion on every element of the array. (Array data is being fetch asynchronously).

Chinn answered 4/9, 2019 at 12:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.