Stubbing Date.now() и Math.random()

Използвам Mocha с Sinon за модулен тест на модулите ми node.js. Успешно се подиграх на други зависимости (други модули, които съм написал), но се натъкнах на проблеми при зарязване на нечисти функции (като Math.random() и Date.now()). Опитах следното (опростено, така че този въпрос да не е толкова локализиран), но Math.random() не беше блокиран поради очевиден проблем с обхвата. Екземплярите на Math са независими между тестовия файл и mymodule.js.

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

Възможно ли е да заглушите Math.random() в този сценарий със Sinon?


person Bailey Parker    schedule 09.06.2013    source източник


Отговори (3)


да, това е стар въпрос, но е валиден. Ето отговор, който работи, въпреки че бих искал да чуя предложения как да го направя по-добър.

Начинът, по който се справих с това в браузъра, е да създам прокси обект. Например, не можете да заглушите обекта прозорец в браузъра, така че можете да създадете прокси обект, наречен windowProxy. Когато искате да получите местоположението, създавате метод в windowProxy, наречен location, който връща или задава windowLocation. След това, когато тествате, вие се подигравате на windowProxy.location.

Можете да направите същото нещо с Node.js, но не работи толкова просто. Простата версия е, че един модул не може да се забърква с частното пространство от имена на друг модул.

Решението е да използвате модула 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.

Важно е да се отбележи, че инструкцията за изискване приема един параметър, './randomHelper'. Ще трябва да отбележим това.

Сега във вашия тест (използвам мока и чай например):

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; Разбира се, можете да го промените.

person newz2000    schedule 04.12.2014
comment
Отличен отговор. Можете също да използвате proxyquire вместо подигравка. - person Wtower; 25.11.2016

Сигурен ли си, че проблемът не е подигравката на Math. Изглежда, че този ред няма много смисъл:

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

подигравате се на others в един модул, но го използвате в друг. Не мисля, че ще получите подиграната версия в mymodule.js. От друга страна заглушаването на Math.random трябва да работи, тъй като това е глобално за всички модули.

Разгледайте и това SO за подигравателни зависимости в nodeJS тестове.

person Andreas Köberle    schedule 09.06.2013
comment
Поправете ме, ако греша, но си помислих, че модулите са кеширани, след като са изискали за първи път. И така, require('./other.js') в тестовия пакет и в кода, който се тества, трябва да бъде един и същ екземпляр. С това мислене предположих, че (като Math.random) подигравката на other в единия ще промени обекта в другия. Но това вероятно не работи, защото присвоява нов обект на друг, вместо да замества свойства. Някакъв начин, който знаете със sinon за заобикаляне на това? - person Bailey Parker; 10.06.2013

Лесно е да заглушите Date.now() с sinon с помощта на Фалшиви таймери:

Фалшивите таймери предоставят часовников обект за преминаване на времето, който също може да се използва за контролиране на Date обекти, създадени чрез new 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();
person Mohamed Ramrami    schedule 12.05.2018