9

我正在使用MochaSinon对我的 node.js 模块进行单元测试。我已经成功地模拟了其他依赖项(我编写的其他模块),但是我遇到了存根非纯函数(如Math.random()Date.now())的问题。我已经尝试了以下方法(已简化,以便这个问题没有那么本地化),但Math.random()由于明显的范围问题而没有被存根。的实例Math在测试文件和mymodule.js.

测试.js

var sinon    = require('sinon'),
    mymodule = require('./mymodule.js'),
    other    = require('./other.js');

describe('MyModule', function() {
    describe('funcThatDependsOnRandom', function() {
        it('should call other.otherFunc with a random num when no num provided', function() {
            sinon.mock(other).expects('otherFunc').withArgs(0.5).once();
            sinon.stub(Math, 'random').returns(0.5);

            funcThatDependsOnRandom(); // called with no args, so should call
                                       // other.otherFunc with random num

            other.verify(); // ensure expectation has been met
        });
    });
});

所以在这个人为的例子中,functThatDependsOnRandom()看起来像:

mymodule.js

var other = require('./other.js');

function funcThatDependsOnRandom(num) {
    if(typeof num === 'undefined') num = Math.random();

    return other.otherFunc(num);
}

Math.random()在这种情况下可以用诗乃存根吗?

4

3 回答 3

9

是的,这是一个老问题,但它是有效的。这是一个可行的答案,尽管我很想听听有关如何使它变得更好的建议。

我在浏览器中处理这个问题的方式是创建一个代理对象。例如,您不能在浏览器中存根窗口对象,因此您可以创建一个名为 windowProxy 的代理对象。当您想要获取位置时,您可以在 windowProxy 中创建一个名为 location 的方法,该方法返回或设置 windowLocation。然后,在测试时,您模拟 windowProxy.location。

你可以用 Node.js 做同样的事情,但它工作起来并不那么简单。简单的版本是一个模块不能与另一个模块的私有命名空间混淆。

解决方案是使用mockery模块。初始化 mockery 后,如果您require()使用与您告诉 mockery 模拟的参数匹配的参数调用,它将允许您覆盖 require 语句并返回您自己的属性。

更新:我创建了一个功能齐全的代码示例。它在Github 上的 newz2000/dice-tdd并且可以通过 npm 获得/结束更新

文档非常好,所以我建议阅读它们,但这里有一个例子:

创建一个randomHelper.js内容如下的文件:

module.exports.random = function() {
  return Math.random();
}

然后在需要随机数的代码中,您:

var randomHelper = require('./randomHelper');

console.log('A random number: ' + randomHelper.random() );

一切都应该正常工作。您的代理对象的行为方式与 Math.random 相同。

重要的是要注意 require 语句接受单个参数'./randomHelper'. 我们需要注意这一点。

现在在您的测试中,(例如,我使用的是 mocha 和 chai):

var sinon = require('sinon');
var mockery = require('mockery')
var yourModule; // note that we didn't require() your module, we just declare it here

describe('Testing my module', function() {

  var randomStub; // just declaring this for now

  before(function() {
    mockery.enable({
      warnOnReplace: false,
      warnOnUnregistered: false
    });

    randomStub = sinon.stub().returns(0.99999);

    mockery.registerMock('./randomHelper', randomStub)
    // note that I used the same parameter that I sent in to requirein the module
    // it is important that these match precisely

    yourmodule = require('../yourmodule');
    // note that we're requiring your module here, after mockery is setup
  }

  after(function() {
    mockery.disable();
  }

  it('Should use a random number', function() {
    callCount = randomStub.callCount;

    yourmodule.whatever(); // this is the code that will use Math.random()

    expect(randomStub.callCount).to.equal(callCount + 1);
  }
}

就是这样。在这种情况下,我们的存根将始终返回 0.0.99999;你当然可以改变它。

于 2014-12-04T20:38:38.943 回答
1

Date.now()使用假定时器很容易存根sinon

假定时器提供了一个时钟对象来传递时间,它也可以用来控制通过 new Date() 创建的 Date 对象;或 Date.now(); (如果浏览器支持)。

// Arrange
const now = new Date();
const clock = sinon.useFakeTimers(now.getTime());

// Act
// Call you function ...

// Assert
// Make some assertions ...

// Teardown
clock.restore();
于 2018-05-12T18:15:59.883 回答
0

你确定不是嘲笑Math是问题所在。这条线似乎没有多大意义:

sinon.mock(other).expects('otherFunc').withArgs(0.5).once();

你在一个模块中模拟others,但在另一个模块中使用它。我认为您不会在mymodule.js. 另一方面,存根 Math.random 应该可以工作,因为这对所有模块都是全局的。

还可以看看这个SO来模拟 nodeJS 测试中的依赖关系。

于 2013-06-09T20:17:38.863 回答