截取 Date.now() 和 Math.random() 方法

10
我正在使用MochaSinon来对我的node.js模块进行单元测试。我已经成功地模拟了其他依赖项(我编写的其他模块),但是在stubbing非纯函数(例如Math.random()Date.now())时遇到了问题。我尝试了以下方法(简化后,以使此问题不再局限于特定情况),但由于明显的作用域问题,Math.random()没有被stubbed。在测试文件和mymodule.js之间,Math的实例是独立的。

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

在这种情况下,使用Sinon是否可以对Math.random()进行存根处理?
3个回答

9

是的,这是一个旧问题,但它仍然有效。下面是一个可行的答案,虽然我很想听听如何使其更好的建议。

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

你可以在Node.js中也做同样的事情,但它不太简单。简单的版本是,一个模块不能干涉另一个模块的私有命名空间。

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

更新: 我已经创建了一个完全功能的代码示例。它在Github上的newz2000/dice-tddnpm可用/END UPDATE

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

创建一个名为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; 当然,您可以更改它。


非常好的答案。您也可以使用proxyquire而不是mockery。 - Wtower

1

使用 sinonFake timers 很容易对 Date.now() 进行桩测试:

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

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

// Act
// Call you function ...

// Assert
// Make some assertions ...

// Teardown
clock.restore();

0
你确定不是在嘲笑Math吗?这行代码似乎没有多大意义。
sinon.mock(other).expects('otherFunc').withArgs(0.5).once();

你在一个模块中嘲笑了其他人,但在另一个模块中使用它。我不认为你会在mymodule.js中得到被嘲笑的版本。另一方面,对Math.random进行存根应该可以工作,因为这是所有模块的全局变量。

此外,请查看SO以了解如何在nodeJS测试中模拟依赖项。


请纠正我,但我认为模块在第一次 require 后会被缓存。因此,在测试套件和被测试的代码中使用 require('./other.js') 应该是相同的实例。基于这种想法,我假设(就像 Math.random 一样)在一个中模拟 other 将修改另一个中的对象。但这可能行不通,因为它将一个新对象分配给了 other,而不是替换属性。您知道 sinon 中有什么方法可以解决这个问题吗? - Bailey Parker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接