2013-06-09 13 views
8

Sto usando Mocha con Sinon sull'unità test i miei moduli node.js. Ho deriso con successo altre dipendenze (altri moduli che ho scritto), ma mi sono imbattuto in problemi di stub non puri (come Math.random() e Date.now()). Ho provato quanto segue (semplificato in modo che questa domanda non sia così localizzata), ma Math.random() non è stato stubato a causa di un evidente problema di portata. Le istanze di Math sono indipendenti tra il file di test e mymodule.js.Stubing Date.now() e Math.random()

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

Quindi, in questo esempio forzato, functThatDependsOnRandom() sarebbe simile:

mymodule.js

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

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

    return other.otherFunc(num); 
} 

E 'possibile stub Math.random() in questo scenario con Sinon?

risposta

7

sì, questa è una vecchia questione, ma è valido. Ecco una risposta che funziona, anche se mi piacerebbe sentire suggerimenti su come migliorarlo.

Il modo in cui ho risolto questo problema nel browser consiste nel creare un oggetto proxy. Ad esempio, non è possibile stubare l'oggetto finestra nel browser in modo da poter creare un oggetto proxy chiamato windowProxy. Quando si desidera ottenere il percorso, creare un metodo in windowProxy chiamato location che restituisce o imposta windowLocation. Quindi, durante il test, si prende in giro windowProxy.location.

Si può fare la stessa cosa con Node.js, ma non funziona altrettanto semplicemente. La versione semplice è che un modulo non può interferire con lo spazio dei nomi privato di un altro modulo.

La soluzione è utilizzare il modulo mockery. Dopo aver inizializzato la beffa, se chiami require() con un parametro che corrisponde a ciò che hai detto a mockery di deridere, ti permetterà di sovrascrivere l'istruzione require e di restituire le tue proprietà.

AGGIORNAMENTO: Ho creato un esempio di codice completamente funzionale. È su Github at newz2000/dice-tdd e available via npm./FINE UPDATE

La documentazione è abbastanza buono, quindi suggerisco leggerli, ma ecco un esempio:

Creare un file randomHelper.js con contenuti in questo modo:

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

Poi nel codice che ha bisogno di un numero casuale, tu:

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

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

Tutto dovrebbe funzionare come normale. Il tuo oggetto proxy si comporta allo stesso modo di Math.random.

È importante notare che l'istruzione require accetta un singolo parametro, './randomHelper'. Dovremo notarlo.

Ora nel test, (sto usando moka e chai per esempio):

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

E questo è tutto. In questo caso, il nostro stub restituirà sempre 0.0.99999; Ovviamente puoi cambiarlo.

+0

Ottima risposta. Puoi usare proxyquire invece di mockery. – Wtower

0

Sei sicuro che non deridere Math è il problema. Sembra che questa linea di make non ha molto senso:

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

si deridere others in un unico modulo, ma utilizza in un altro. Non penso che otterrete la versione derisoria in mymodule.js. D'altra parte, la stesura di Math.random dovrebbe funzionare, poiché è globale per tutti i moduli.

Dai un'occhiata anche a questo SO per le dipendenze di derisione nei test di nodeJS.

+0

Correggetemi se ho torto, ma ho pensato che i moduli fossero memorizzati nella cache dopo che i loro bisogni hanno richiesto. Quindi, 'require ('./ other.js')' nella suite di test e nel codice da testare dovrebbe essere la stessa istanza. Con questo pensiero, ho assunto che (come 'Math.random') il mocking' other' in uno modificherebbe l'oggetto nell'altro. Ma probabilmente non funziona perché assegna un nuovo oggetto ad altri invece di sostituire le proprietà. Qualche modo che sai con sinon di aggirare questo? –

1

Prova:

sinon.stub(Math, "random", function(){ 
    return 0.5; 
    }); 
+2

Questo non funziona quando la funzione che utilizza Math.random() si trova in un altro modulo. Funziona quando si utilizza REPL o la funzione è nello stesso file .js. – newz2000