Come posso prendere in giro le dipendenze per i test unitari in RequireJS?


127

Ho un modulo AMD che voglio testare, ma voglio deridere le sue dipendenze invece di caricare le dipendenze effettive. Sto usando requestjs e il codice per il mio modulo è simile al seguente:

define(['hurp', 'durp'], function(Hurp, Durp) {
  return {
    foo: function () {
      console.log(Hurp.beans)
    },
    bar: function () {
      console.log(Durp.beans)
    }
  }
}

Come posso deridere hurpe durpquindi posso effettivamente testare l'unità?


Sto solo facendo delle cose folli di eval in node.js per deridere la definefunzione. Ci sono alcune diverse opzioni però. Pubblicherò una risposta nella speranza che possa essere utile.
jergason,

1
Per test unitari con Jasmine potresti anche dare un'occhiata a Jasq . [Disclaimer: sto mantenendo la lib]
biril

1
Se stai testando in env nodo puoi usare il pacchetto request-mock . Ti consente di deridere facilmente le tue dipendenze, sostituire i moduli ecc. Se hai bisogno di un browser env con caricamento del modulo asincrono - potresti provare Squire.js
ValeriiVasin

Risposte:


64

Quindi, dopo aver letto questo post, mi è venuta in mente una soluzione che utilizza la funzione di configurazione requestjs per creare un nuovo contesto per il test in cui puoi semplicemente deridere le tue dipendenze:

var cnt = 0;
function createContext(stubs) {
  cnt++;
  var map = {};

  var i18n = stubs.i18n;
  stubs.i18n = {
    load: sinon.spy(function(name, req, onLoad) {
      onLoad(i18n);
    })
  };

  _.each(stubs, function(value, key) {
    var stubName = 'stub' + key + cnt;

    map[key] = stubName;

    define(stubName, function() {
      return value;
    });
  });

  return require.config({
    context: "context_" + cnt,
    map: {
      "*": map
    },
    baseUrl: 'js/cfe/app/'
  });
}

Quindi crea un nuovo contesto in cui le definizioni per Hurpe Durpverranno impostate dagli oggetti passati nella funzione. Il Math.random per il nome è forse un po 'sporco ma funziona. Perché se avrai un sacco di test devi creare un nuovo contesto per ogni suite per evitare di riutilizzare le tue beffe o per caricare le beffe quando vuoi il vero modulo requestjs.

Nel tuo caso sarebbe simile a questo:

(function () {

  var stubs =  {
    hurp: 'hurp',
    durp: 'durp'
  };
  var context = createContext(stubs);

  context(['yourModuleName'], function (yourModule) {

    //your normal jasmine test starts here

    describe("yourModuleName", function () {
      it('should log', function(){
         spyOn(console, 'log');
         yourModule.foo();

         expect(console.log).toHasBeenCalledWith('hurp');
      })
    });
  });
})();

Quindi sto usando questo approccio in produzione da un po 'ed è davvero robusto.


1
Mi piace quello che stai facendo qui ... soprattutto perché puoi caricare un contesto diverso per ogni test. L'unica cosa che vorrei poter cambiare è che sembra funzionare solo se prendo in giro tutte le dipendenze. Conoscete un modo per restituire gli oggetti finti se sono presenti, ma fallback per recuperare dal file .js reale se non viene fornito un finto? Ho provato a cercare il codice richiesto per capirlo, ma mi sto perdendo un po '.
Glen Hughes,

5
Deride solo la dipendenza che si passa alla createContextfunzione. Quindi, nel tuo caso, se passi solo {hurp: 'hurp'}alla funzione, il durpfile verrà caricato come una normale dipendenza.
Andreas Köberle,

1
Sto usando questo in Rails (con jasminerice / phantomjs) ed è stata la migliore soluzione che ho trovato per deridere con RequireJS.
Ben Anderson,

13
+1 Non carina, ma tra tutte le possibili soluzioni questa sembra essere la meno brutta / disordinata. Questo problema merita più attenzione.
Chris Salzberg,

1
Aggiornamento: a chiunque stia considerando questa soluzione, suggerirei di dare un'occhiata a squire.js ( github.com/iammerrick/Squire.js ) di seguito indicato. È una bella implementazione di una soluzione simile a questa, che crea nuovi contesti ovunque siano necessari stub.
Chris Salzberg,

44

potresti voler dare un'occhiata alla nuova libreria Squire.js

dai documenti:

Squire.js è un iniettore di dipendenze per gli utenti Require.js per semplificare le dipendenze beffardo!


2
Fortemente raccomandato! Sto aggiornando il mio codice per usare squire.js e finora mi piace molto. Codice molto molto semplice, nessuna grande magia sotto il cofano, ma fatto in un modo (relativamente) facile da capire.
Chris Salzberg,

1
Ho avuto molti problemi con gli effetti collaterali di scudiero su altri test e non posso raccomandarlo. Consiglierei npmjs.com/package/requirejs-mock
Jeff Whiting il

17

Ho trovato tre diverse soluzioni a questo problema, nessuna piacevole.

Definizione delle dipendenze in linea

define('hurp', [], function () {
  return {
    beans: 'Beans'
  };
});

define('durp', [], function () {
  return {
    beans: 'durp beans'
  };
});

require('hurpdhurp', function () {
  // test hurpdurp in here
});

Fugly. Devi ingombrare i tuoi test con un sacco di boiler AMD.

Caricamento di dipendenze fittizie da percorsi diversi

Ciò implica l'uso di un file config.js separato per definire i percorsi per ciascuna delle dipendenze che puntano a simulazioni invece delle dipendenze originali. Anche questo è brutto, che richiede la creazione di tonnellate di file di test e file di configurazione.

Falso nel nodo

Questa è la mia soluzione attuale, ma è ancora terribile.

Si crea la propria definefunzione per fornire le proprie beffe al modulo e mettere i test nel callback. Quindi evalil modulo per eseguire i test, in questo modo:

var fs = require('fs')
  , hurp = {
      beans: 'BEANS'
    }
  , durp = {
      beans: 'durp beans'
    }
  , hurpDurp = fs.readFileSync('path/to/hurpDurp', 'utf8');
  ;



function define(deps, cb) {
  var TestableHurpDurp = cb(hurp, durp);
  // now run tests below on TestableHurpDurp, which is using your
  // passed-in mocks as dependencies.
}

// evaluate the AMD module, running your mocked define function and your tests.
eval(hurpDurp);

Questa è la mia soluzione preferita. Sembra un po 'magico, ma ha alcuni vantaggi.

  1. Esegui i test nel nodo, quindi non scherzare con l'automazione del browser.
  2. Minore necessità di disordinata piastra AMD nei test.
  3. Puoi usare la evalrabbia e immaginare che Crockford esploda di rabbia.

Ha ancora alcuni inconvenienti, ovviamente.

  1. Dato che stai testando nel nodo, non puoi fare nulla con gli eventi del browser o la manipolazione del DOM. Buono solo per testare la logica.
  2. Ancora un po 'goffo da installare. È necessario prendere in giro defineogni test, poiché è lì che i test vengono effettivamente eseguiti.

Sto lavorando su un test runner per fornire una sintassi migliore per questo tipo di cose, ma non ho ancora una buona soluzione per il problema 1.

Conclusione

Deridere beffardi in requestjs fa schifo. Ho trovato il modo in cui funziona il sortof, ma non sono ancora molto soddisfatto. Per favore fatemi sapere se avete idee migliori.


15

C'è config.mapun'opzione http://requirejs.org/docs/api.html#config-map .

Su come usarlo:

  1. Definire il modulo normale;
  2. Definire il modulo stub;
  3. Configurare RequireJS in modo rapido;

    requirejs.config({
      map: {
        'source/js': {
          'foo': 'normalModule'
        },
        'source/test': {
          'foo': 'stubModule'
        }
      }
    });

In questo caso, per il codice normale e di test, è possibile utilizzare il foomodulo che sarà riferimento reale del modulo e stub di conseguenza.


Questo approccio ha funzionato davvero bene per me. Nel mio caso, l'ho aggiunto all'html della pagina del runner di test -> map: {'*': {'Common / Modules / UsefulModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Allineato

9

È possibile utilizzare testr.js per deridere le dipendenze. È possibile impostare testr per caricare le dipendenze simulate invece di quelle originali. Ecco un esempio di utilizzo:

var fakeDep = function(){
    this.getText = function(){
        return 'Fake Dependancy';
    };
};

var Module1 = testr('module1', {
    'dependancies/dependancy1':fakeDep
});

Dai un'occhiata anche a questo: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/


2
Volevo davvero che testr.js funzionasse, ma non mi sento ancora all'altezza del compito. Alla fine vado con la soluzione di @Andreas Köberle, che aggiungerà contesti nidificati ai miei test (non abbastanza) ma che funziona costantemente. Vorrei che qualcuno potesse concentrarsi sulla risoluzione di questa soluzione in un modo più elegante. Continuerò a guardare testr.js e se / quando funzionerà, cambierà.
Chris Salzberg,

@shioyama ciao, grazie per il feedback! Mi piacerebbe dare un'occhiata a come hai configurato testr.js nel tuo stack di test. Felice di aiutarti a risolvere eventuali problemi che potresti avere! C'è anche la pagina dei problemi di github se vuoi registrare qualcosa lì. Grazie,
Matty F

1
@MattyF scusami non ricordo nemmeno in questo momento quale sia il motivo esatto per cui testr.js non ha funzionato per me, ma sono giunto alla conclusione che l'uso di contesti extra è in realtà abbastanza corretto e in effetti in linea con come si richiedeva use.js per deridere / stub.
Chris Salzberg,

2

Questa risposta si basa sulla risposta di Andreas Köberle .
Non è stato facile per me implementare e comprendere la sua soluzione, quindi lo spiegherò un po 'più in dettaglio come funziona e alcune insidie ​​da evitare, sperando che possa aiutare i futuri visitatori.

Quindi, prima di tutto l'installazione:
sto usando Karma come test runner e MochaJs come framework di test.

Usare qualcosa come Squire non ha funzionato per me, per qualche ragione, quando l'ho usato, il framework di test ha generato errori:

TypeError: impossibile leggere la proprietà 'call' di undefined

RequireJs ha la possibilità di mappare gli id del modulo su altri ID del modulo. Inoltre, consente di creare una requirefunzione che utilizza una configurazione diversa da quella globale require.
Queste funzionalità sono fondamentali per il funzionamento di questa soluzione.

Ecco la mia versione del codice simulato, inclusi (molti) commenti (spero che sia comprensibile). L'ho avvolto in un modulo, in modo che i test possano facilmente richiederlo.

define([], function () {
    var count = 0;
    var requireJsMock= Object.create(null);
    requireJsMock.createMockRequire = function (mocks) {
        //mocks is an object with the module ids/paths as keys, and the module as value
        count++;
        var map = {};

        //register the mocks with unique names, and create a mapping from the mocked module id to the mock module id
        //this will cause RequireJs to load the mock module instead of the real one
        for (property in mocks) {
            if (mocks.hasOwnProperty(property)) {
                var moduleId = property;  //the object property is the module id
                var module = mocks[property];   //the value is the mock
                var stubId = 'stub' + moduleId + count;   //create a unique name to register the module

                map[moduleId] = stubId;   //add to the mapping

                //register the mock with the unique id, so that RequireJs can actually call it
                define(stubId, function () {
                    return module;
                });
            }
        }

        var defaultContext = requirejs.s.contexts._.config;
        var requireMockContext = { baseUrl: defaultContext.baseUrl };   //use the baseUrl of the global RequireJs config, so that it doesn't have to be repeated here
        requireMockContext.context = "context_" + count;    //use a unique context name, so that the configs dont overlap
        //use the mapping for all modules
        requireMockContext.map = {
            "*": map
        };
        return require.config(requireMockContext);  //create a require function that uses the new config
    };

    return requireJsMock;
});

La più grande trappola che ho incontrato, che mi è costato letteralmente ore, è stata la creazione della configurazione di RequireJs. Ho provato a copiarlo (in profondità) e ho solo la precedenza sulle proprietà necessarie (come il contesto o la mappa). Questo non funziona! Copia solo il baseUrl, questo funziona bene.

uso

Per usarlo, richiedilo nel tuo test, crea le beffe e poi passalo a createMockRequire. Per esempio:

var ModuleMock = function () {
    this.method = function () {
        methodCalled += 1;
    };
};
var mocks = {
    "ModuleIdOrPath": ModuleMock
}
var requireMocks = mocker.createMockRequire(mocks);

E qui un esempio di un file di test completo :

define(["chai", "requireJsMock"], function (chai, requireJsMock) {
    var expect = chai.expect;

    describe("Module", function () {
        describe("Method", function () {
            it("should work", function () {
                return new Promise(function (resolve, reject) {
                    var handler = { handle: function () { } };

                    var called = 0;
                    var moduleBMock = function () {
                        this.method = function () {
                            methodCalled += 1;
                        };
                    };
                    var mocks = {
                        "ModuleBIdOrPath": moduleBMock
                    }
                    var requireMocks = requireJsMock.createMockRequire(mocks);

                    requireMocks(["js/ModuleA"], function (moduleA) {
                        try {
                            moduleA.method();   //moduleA should call method of moduleBMock
                            expect(called).to.equal(1);
                            resolve();
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            });
        });
    });
});

0

se vuoi fare dei semplici test js che isolano un'unità, puoi semplicemente usare questo snippet:

function define(args, func){
    if(!args.length){
        throw new Error("please stick to the require.js api which wants a: define(['mydependency'], function(){})");
    }

    var fileName = document.scripts[document.scripts.length-1].src;

    // get rid of the url and path elements
    fileName = fileName.split("/");
    fileName = fileName[fileName.length-1];

    // get rid of the file ending
    fileName = fileName.split(".");
    fileName = fileName[0];

    window[fileName] = func;
    return func;
}
window.define = define;
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.