Come testare un'unità di un modulo Node.js che richiede altri moduli e come deridere la funzione di richiesta globale?


156

Questo è un esempio banale che illustra il nocciolo del mio problema:

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

Sto cercando di scrivere un test unitario per questo codice. Come posso deridere il requisito per il innerLibsenza deridere completamente la requirefunzione?

Quindi sono io che sto cercando di deridere il globale requiree scoprire che non funzionerà nemmeno per farlo:

var path = require('path'),
    vm = require('vm'),
    fs = require('fs'),
    indexPath = path.join(__dirname, './underTest');

var globalRequire = require;

require = function(name) {
    console.log('require: ' + name);
    switch(name) {
        case 'connect':
        case indexPath:
            return globalRequire(name);
            break;
    }
};

Il problema è che la requirefunzione all'interno del underTest.jsfile non è stata effettivamente derisa. Indica ancora la requirefunzione globale . Quindi sembra che posso solo deridere la requirefunzione all'interno dello stesso file in cui sto eseguendo il derisione. Se uso il globale requireper includere qualcosa, anche dopo aver sovrascritto la copia locale, i file richiesti avranno comunque il requireriferimento globale .


devi sovrascrivere global.require. Le variabili scrivono per moduleimpostazione predefinita poiché i moduli hanno ambito del modulo.
Raynos,

@Raynos Come lo farei? global.require non è definito? Anche se lo sostituissi con la mia funzione, altre funzioni non lo utilizzerebbero mai?
HMR,

Risposte:


175

Tu puoi ora!

Ho pubblicato proxyquire che si occuperà di sovrascrivere il requisito globale all'interno del modulo durante il test.

Ciò significa che non è necessario apportare modifiche al codice per iniettare mock per i moduli richiesti.

Proxyquire ha un'API molto semplice che consente di risolvere il modulo che si sta tentando di testare e di passare lungo mock / stub per i moduli richiesti in un semplice passaggio.

@Raynos ha ragione nel fatto che tradizionalmente dovevi ricorrere a soluzioni non molto ideali per raggiungere questo obiettivo o fare uno sviluppo dal basso

Questo è il motivo principale per cui ho creato proxyquire: per consentire lo sviluppo guidato da test top-down senza problemi.

Dai un'occhiata alla documentazione e agli esempi per valutare se si adatta alle tue esigenze.


5
Uso proxyquire e non posso dire abbastanza cose buone. Mi ha salvato! Mi è stato affidato il compito di scrivere test su nodi di gelsomino per un'app sviluppata in appaniumer Titanium che forza alcuni moduli ad essere percorsi assoluti e molte dipendenze circolari. proxyquire mi ha permesso di smettere di dividerli e di deridere l'innesto che non avevo bisogno di ogni test. (Spiegato qui ). Grazie mille!
Sukima,

Felice di sapere che proxyquire ti ha aiutato a testare correttamente il tuo codice :)
Thorsten Lorenz,

1
molto bello @ThorstenLorenz, lo farò. sta usando proxyquire!
Bevacqua

Fantastico! Quando ho visto la risposta accettata che "non puoi" ho pensato "Oh Dio, sul serio ?!" ma questo l'ha salvato davvero.
Chadwick,

3
Per quelli di voi che usano Webpack, non perdere tempo a cercare proxyquire. Non supporta Webpack. Sto invece cercando inject-loader ( github.com/plasticine/inject-loader ).
Artif3x,

116

Un'opzione migliore in questo caso è quella di deridere i metodi del modulo che viene restituito.

Nel bene o nel male, la maggior parte dei moduli node.js sono singoli; due pezzi di codice che richiedono () lo stesso modulo ottengono lo stesso riferimento a quel modulo.

Puoi sfruttare questo e usare qualcosa come Sinon per deridere gli oggetti che sono richiesti. segue il test moka :

// in your testfile
var innerLib  = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon     = require('sinon');

describe("underTest", function() {
  it("does something", function() {
    sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
      // whatever you would like innerLib.toCrazyCrap to do under test
    });

    underTest();

    sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion

    innerLib.toCrazyCrap.restore(); // restore original functionality
  });
});

Sinon ha una buona integrazione con Chai per fare affermazioni, e ho scritto un modulo per integrare sinon con moka per consentire una più facile pulizia spia / stub (per evitare il test dell'inquinamento).

Nota che UnderTest non può essere deriso allo stesso modo, poiché UnderTest restituisce solo una funzione.

Un'altra opzione è usare le beffe di Jest. Segui sulla loro pagina


1
Sfortunatamente, i moduli node.js NON sono garantiti come singoli, come spiegato qui: justjs.com/posts/…
FrontierPsycho

4
@FrontierPsycho alcune cose: in primo luogo, per quanto riguarda i test, l'articolo è irrilevante. Fintanto che stai testando le tue dipendenze (e non le dipendenze delle dipendenze) tutto il tuo codice require('some_module')recupererà lo stesso oggetto quando te , perché tutto il tuo codice condivide la stessa directory node_modules. In secondo luogo, l'articolo sta fondendo lo spazio dei nomi con i singoli, che è una specie di ortogonale. In terzo luogo, l'articolo è dannatamente vecchio (per quanto riguarda node.js), quindi ciò che potrebbe essere stato valido in passato non è probabilmente valido ora.
Elliot Foster,

2
Hm. A meno che uno di noi non prenda effettivamente il codice che provi un punto o l'altro, andrei con la tua soluzione di iniezione di dipendenza, o semplicemente passando oggetti in giro, è più sicuro e più a prova di futuro.
FrontierPsycho

1
Non sono sicuro di cosa stai chiedendo di essere provato. La natura singleton (memorizzata nella cache) dei moduli del nodo è comunemente compresa. L'iniezione di dipendenza, sebbene sia un buon percorso, può essere una buona dose di più piastra della caldaia e più codice. DI è più comune nei linguaggi tipicamente statici, in cui è più difficile infilzare spie / stub / derisioni nel codice in modo dinamico. Più progetti che ho realizzato negli ultimi tre anni utilizzano il metodo descritto nella mia risposta sopra. È il metodo più semplice di tutti, anche se lo uso con parsimonia.
Elliot Foster,

1
Ti suggerisco di leggere su sinon.js. Se stai usando Sinon (come nell'esempio sopra) si farebbe neanche innerLib.toCrazyCrap.restore()e restub, o chiamare Sinon via sinon.stub(innerLib, 'toCrazyCrap')che consente di modificare il modo le si comporta stub: innerLib.toCrazyCrap.returns(false). Inoltre, rewire sembra essere molto simile proxyquireall'estensione sopra.
Elliot Foster,

11

Uso il finto requisito . Assicurati di definire i tuoi mock prima di te requireil modulo da testare.


Anche buono da fare stop (<file>) o stopAll () immediatamente in modo da non ottenere un file memorizzato nella cache in un test in cui non si desidera la simulazione.
Justin Kruse,

1
Questo ha aiutato moltissimo.
Wallop,

2

beffardo require mi sembra un brutto scherzo. Personalmente proverei ad evitarlo e refactoring il codice per renderlo più testabile. Esistono vari approcci per gestire le dipendenze.

1) passare dipendenze come argomenti

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

Ciò renderà il codice universalmente testabile. Il rovescio della medaglia è che è necessario passare dipendenze, il che può rendere il codice più complicato.

2) implementare il modulo come classe, quindi utilizzare metodi / proprietà della classe per ottenere dipendenze

(Questo è un esempio inventato, in cui l'uso della classe non è ragionevole, ma trasmette l'idea) (esempio ES6)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

Ora puoi facilmente bloccare il getInnerLibmetodo per testare il tuo codice. Il codice diventa più dettagliato, ma anche più facile da testare.


1
Non penso sia confuso come presumi ... questa è l'essenza del deridere. Deridere le dipendenze richieste rende le cose così semplici da dare il controllo allo sviluppatore senza cambiare la struttura del codice. I tuoi metodi sono troppo dettagliati e quindi difficili da ragionare. Scelgo proxyrequire o mock-request su questo; non vedo alcun problema qui. Il codice è pulito e facile da ragionare e ricordare che molte persone che leggono questo hanno già un codice scritto che vuoi complicare. Se queste librerie sono hack, allora beffardo e mobbing sono anche hack dalla tua definizione e dovrebbero essere fermati.
Emmanuel Mahuni,

1
Il problema con l'approccio n. 1 è che si stanno passando i dettagli dell'implementazione interna nello stack. Con più livelli diventa quindi molto più complicato essere un consumatore del tuo modulo. Può funzionare con un approccio simile al contenitore IOC, tuttavia in modo che le dipendenze vengano automaticamente iniettate per te, tuttavia sembra che poiché abbiamo già delle dipendenze iniettate nei moduli del nodo tramite l'istruzione import, quindi ha senso essere in grado di deriderle a quel livello .
magritte,

1) Questo sposta semplicemente il problema in un altro file 2) carica comunque l'altro modulo e impone quindi un sovraccarico prestazionale, e probabilmente causa effetti collaterali (come il popolare colorsmodulo con cui si String.prototype
scherza

2

Se hai mai usato jest, allora probabilmente hai familiarità con la funzionalità di jest.

Usando "jest.mock (...)" puoi semplicemente specificare la stringa che si verificherebbe da qualche parte in un'istruzione request nel tuo codice e ogni volta che un modulo è richiesto usando quella stringa verrebbe invece restituito un oggetto simulato.

Per esempio

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

sostituirà completamente tutte le importazioni / i requisiti di "firebase-admin" con l'oggetto restituito da quella funzione "factory".

Bene, puoi farlo quando usi jest perché jest crea un runtime attorno a ogni modulo che esegue e inietta una versione "hooked" di request nel modulo, ma non saresti in grado di farlo senza jest.

Ho cercato di raggiungere questo obiettivo con finto requisito, ma per me non ha funzionato per i livelli nidificati nella mia fonte. Dai un'occhiata al seguente numero su github: mock-request non sempre chiamato con Mocha .

Per risolvere questo problema, ho creato due moduli npm che è possibile utilizzare per ottenere ciò che si desidera.

Hai bisogno di un plug-in per babele e un mocker di moduli.

Nel tuo .babelrc usa il plug-in babel-plugin-mock-require con le seguenti opzioni:

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

e nel tuo file di test usa il modulo jestlike-mock in questo modo:

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

Il jestlike-mock modulo è ancora molto rudimentale e non ha molta documentazione ma non c'è neanche molto codice. Apprezzo qualsiasi PR per un set di funzionalità più completo. L'obiettivo sarebbe quello di ricreare l'intera funzione "jest.mock".

Per vedere come jest implementa la possibilità di cercare il codice nel pacchetto "jest-runtime". Vedi https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 per esempio, qui generano un "automock" di un modulo.

Spero che aiuti ;)


1

Non puoi. È necessario creare la suite di test delle unità in modo che i moduli più bassi vengano testati per primi e che i moduli di livello superiore che richiedono moduli vengano successivamente testati.

Devi anche supporre che qualsiasi codice di terze parti e node.js stesso siano ben testati.

Presumo che nel prossimo futuro arriveranno quadri beffardi che sovrascrivono global.require

Se devi davvero iniettare una simulazione, puoi modificare il codice per esporre l'ambito modulare.

// underTest.js
var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.toCrazyCrap();
}

module.exports = underTest;
module.exports.__module = module;

// test.js
function test() {
    var underTest = require("underTest");
    underTest.__module.innerLib = {
        toCrazyCrap: function() { return true; }
    };
    assert.ok(underTest());
}

Tieni presente che ciò si espone .__modulenella tua API e qualsiasi codice può accedere all'ambito modulare a proprio rischio.


2
Supponendo che il codice di terze parti sia ben testato non è un ottimo modo per lavorare con IMO.
henry.oswald,

5
@beck è un ottimo modo di lavorare. Ti costringe a lavorare solo con codice di terze parti di alta qualità o scrivere tutti i pezzi del tuo codice in modo che ogni dipendenza sia ben testata
Raynos,

Ok, pensavo che ti riferissi a non fare test di integrazione tra il tuo codice e il codice di terze parti. Concordato.
henry.oswald,

1
Una "suite di test unitari" è solo una raccolta di test unitari, ma i test unitari dovrebbero essere indipendenti l'uno dall'altro, quindi l'unità nel test unitario. Per essere utilizzabili, i test unitari devono essere rapidi e indipendenti, in modo da poter vedere chiaramente dove il codice è rotto quando un test unitario fallisce.
Andreas Berheim Brudin,

Questo non ha funzionato per me. L'oggetto modulo non espone il "var innerLib ..." ecc.
AnitKryst,

1

È possibile utilizzare la libreria di derisione :

describe 'UnderTest', ->
  before ->
    mockery.enable( warnOnUnregistered: false )
    mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
    @underTest = require('./path/to/underTest')

  it 'should compute complex value', ->
    expect(@underTest()).to.eq 'Complex result'

1

Codice semplice per deridere i moduli per i curiosi

Nota le parti in cui manipoli il metodo require.cachee note in require.resolvequanto questa è la salsa segreta.

class MockModules {  
  constructor() {
    this._resolvedPaths = {} 
  }
  add({ path, mock }) {
    const resolvedPath = require.resolve(path)
    this._resolvedPaths[resolvedPath] = true
    require.cache[resolvedPath] = {
      id: resolvedPath,
      file: resolvedPath,
      loaded: true,
      exports: mock
    }
  }
  clear(path) {
    const resolvedPath = require.resolve(path)
    delete this._resolvedPaths[resolvedPath]
    delete require.cache[resolvedPath]
  }
  clearAll() {
    Object.keys(this._resolvedPaths).forEach(resolvedPath =>
      delete require.cache[resolvedPath]
    )
    this._resolvedPaths = {}
  }
}

Usa come :

describe('#someModuleUsingTheThing', () => {
  const mockModules = new MockModules()
  beforeAll(() => {
    mockModules.add({
      // use the same require path as you normally would
      path: '../theThing',
      // mock return an object with "theThingMethod"
      mock: {
        theThingMethod: () => true
      }
    })
  })
  afterAll(() => {
    mockModules.clearAll()
  })
  it('should do the thing', async () => {
    const someModuleUsingTheThing = require('./someModuleUsingTheThing')
    expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
  })
})

MA ... proxyquire è davvero fantastico e dovresti usarlo. Mantiene le tue sostituzioni localizzate solo ai test e lo consiglio vivamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.