如何多次断言存根提取

dma*_*man 7 javascript unit-testing node.js sinon proxyquire

使用proxyquire,sinon和mocha.

我能够在第一次调用fetch时获取存根.但是在第二次提取调用中,这是递归的,我无法断言它.从输出看,断言可能在测试结束之前运行.second fetch在断言后你会看到控制台输出.

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()
Run Code Online (Sandbox Code Playgroud)

测试:

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()
  });

});
Run Code Online (Sandbox Code Playgroud)
   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)
Run Code Online (Sandbox Code Playgroud)

spi*_*lio 3

更新后的版本

@dman,自从您更新了测试用例以来,我欠您一个更新的答案。虽然经过重新表述,但这个场景仍然是非正统的——在某种意义上,你似乎想忽略“万有引力定律”,尽管你知道它就在你面前。

我会尽可能地进行描述。您有两个按设计 执行异步操作的函数。a()按顺序调用b() - 顺便说一句,这不是递归。这两个函数在完成/失败时都不会通知其调用者,即它们被视为“即发即弃”

现在,让我们看看您的测试场景。您创建 3 个存根。其中两个解析为字符串,一个使用 组合它们的执行Promise.all()。接下来,您代理“node-fetch”模块

proxy('./updated', {
    'node-fetch': fetch
});
Run Code Online (Sandbox Code Playgroud)

使用返回存根 1 和 2 的组合执行的存根。现在,如果您在任一函数中打印出fetch的解析值,您将看到它不是字符串,而是存根数组。

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

我猜这不是预期的输出。但让我们继续吧,因为无论如何这不会杀死你的测试。接下来,您将断言与did()语句一起添加。

fetch.should.have.been.callCount(2);
done();
Run Code Online (Sandbox Code Playgroud)

这里的问题是,无论你是否使用done(),效果都是完全相同的。您正在同步模式下执行您的场景。当然,在这种情况下,断言总是会失败。但这里重要的是了解原因

因此,让我们重写您的场景以模仿您要验证的行为的异步性质。

'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);

    });

});
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,所做的唯一更改是将断言包装在计时器块中。没什么大不了的——只需等待 10 毫秒,然后断言即可。现在测试按预期通过了。为什么?

嗯,对我来说这非常简单。您想要测试 2 个顺序执行的异步函数,并且仍然在同步模式下运行您的断言。这听起来很酷,但这不会发生:)所以你有两个选择:

  • 让您的函数在完成时通知调用者,然后以真正的异步模式运行您的断言
  • 使用非正统技术模仿事物的异步本质

根据原始测试场景回复

可以办到。我已经对您提供的文件进行了一些重构,以便可以执行。

索引.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);
          });

  });
};
Run Code Online (Sandbox Code Playgroud)

警报.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);
      });

  });
};
Run Code Online (Sandbox Code Playgroud)

测试.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));

  });

});
Run Code Online (Sandbox Code Playgroud)

不过,当涉及到良好的单元测试实践时,您尝试做的事情有点非正统。尽管proxyquire通过称为全局覆盖的功能支持这种存根模式,但 这里解释了为什么任何人在走这条路之前都应该三思而后行。

为了使您的示例通过测试,您只需向 Sinon 存根添加一个名为@global的额外属性并将其设置为 true。该标志会覆盖require()缓存机制,并使用提供的存根,无论从哪个模块调用。

因此,尽管您所要求的可以完成,但我必须同意评论您的问题的用户,但这不应该被采用作为构建测试的正确方法。