web-dev-qa-db-fra.com

Comment puis-je simuler des dépendances pour les tests unitaires dans RequireJS?

Je souhaite tester un module AMD, mais je veux simuler ses dépendances au lieu de charger les dépendances réelles. J'utilise requirejs, et le code de mon module ressemble à ceci:

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

Comment puis-je simuler hurp et durp afin de pouvoir effectuer des tests unitaires?

126
jergason

Donc, après avoir lu cet article , je suis arrivé à une solution qui utilise la fonction de configuration requirejs pour créer un nouveau contexte pour votre test où vous pouvez simplement vous moquer de vos dépendances:

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

Cela crée donc un nouveau contexte dans lequel les définitions de Hurp et Durp seront définies par les objets que vous avez passés dans la fonction. Le nom Math.random est peut-être un peu sale, mais ça marche. Parce que si vous avez un tas de tests, vous devez créer un nouveau contexte pour chaque suite pour éviter de réutiliser vos simulacres, ou pour charger des simulacres lorsque vous voulez le module requirejs réel.

Dans votre cas, cela ressemblerait à ceci:

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

Donc, j'utilise cette approche en production depuis un moment et c'est vraiment robuste.

64
Andreas Köberle

vous voudrez peut-être consulter le nouveau Squire.js lib

des docs:

Squire.js est un injecteur de dépendances permettant aux utilisateurs de Require.js de simplifier les dépendances moqueuses!

44
busticated

J'ai trouvé trois solutions différentes à ce problème, aucune d'entre elles n'étant agréable.

Définition de dépendances en ligne

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

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

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

Fugly. Vous devez encombrer vos tests avec de nombreuses techniques AMD.

Chargement de dépendances factices à partir de différents chemins

Cela implique l'utilisation d'un fichier séparé config.js pour définir des chemins pour chacune des dépendances qui pointent vers des alarmes au lieu des dépendances d'origine. C'est également moche, nécessitant la création de tonnes de fichiers de test et de fichiers de configuration.

Fake It In Node

Ceci est ma solution actuelle, mais est toujours une terrible.

Vous créez votre propre fonction define pour fournir vos propres modèles au module et mettre vos tests dans le rappel. Ensuite, vous eval le module pour exécuter vos tests, comme ceci:

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

Ceci est ma solution préférée. Cela semble un peu magique, mais présente quelques avantages.

  1. Exécutez vos tests dans le noeud, donc ne vous mêlez pas de l'automatisation du navigateur.
  2. Moins de besoin de passe-partout malpropre dans vos tests.
  3. Vous pouvez utiliser eval dans la colère et imaginez Crockford exploser de rage.

Il y a toujours des inconvénients, évidemment.

  1. Étant donné que vous testez dans node, vous ne pouvez rien faire avec les événements de navigateur ou la manipulation DOM. Seulement bon pour tester la logique.
  2. Encore un peu maladroit à mettre en place. Vous devez simuler define dans chaque test, car c’est là que s’effectuent réellement vos tests.

Je travaille sur un coureur de test pour donner une syntaxe plus agréable pour ce genre de choses, mais je n'ai toujours pas de bonne solution pour le problème 1.

Conclusion

Mocking Deps dans requirejs suce dur. J'ai trouvé un moyen qui fonctionne, mais je n'en suis toujours pas très heureux. S'il vous plaît laissez-moi savoir si vous avez de meilleures idées.

16
jergason

Il y a un config.map option http://requirejs.org/docs/api.html#config-map .

Sur comment l'utiliser:

  1. Définir le module normal;
  2. Définir le module de remplacement;
  3. Configurez RequireJS de manière explicite;

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

Dans ce cas, pour le code normal et le code de test, vous pouvez utiliser le module foo qui sera la référence du module réel et le stub correspondant.

15
Artem Oboturov

Vous pouvez utiliser testr.js pour simuler des dépendances. Vous pouvez configurer testr pour charger les dépendances factices au lieu de celles d'origine. Voici un exemple d'utilisation:

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

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

Regardez aussi ceci: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

9
janith

Cette réponse est basée sur réponse d'Andreas Köberle .
Ce n’était pas si facile pour moi d’implémenter et de comprendre sa solution. Je vais donc expliquer un peu plus en détail comment cela fonctionne, ainsi que certains pièges à éviter, en espérant que cela aidera les futurs visiteurs.

Donc, tout d’abord, la configuration:
J'utilise le Karma comme coureur de test et MochaJs comme cadre de test.

Utiliser quelque chose comme Squire n'a pas fonctionné pour moi. Pour une raison quelconque, lorsque je l'ai utilisé, le cadre de test a généré des erreurs:

TypeError: Impossible de lire la propriété 'appel' d'undefined

RequireJs a la possibilité de mapper identifiants de module avec d'autres identifiants de module. Cela permet également de créer une fonction require qui utilise un configuration différente que le global require.
Ces fonctionnalités sont essentielles au bon fonctionnement de cette solution.

Voici ma version du code factice, y compris (beaucoup) des commentaires (j'espère que c'est compréhensible). Je l'ai enveloppé dans un module pour que les tests puissent l'exiger facilement.

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

Le plus gros piège que j'ai rencontré, qui m'a coûté littéralement des heures, était la création de la configuration RequireJs. J'ai essayé de le copier (profondément) et de ne remplacer que les propriétés nécessaires (comme le contexte ou la carte). Cela ne fonctionne pas! Copiez seulement le baseUrl, cela fonctionne bien.

Usage

Pour l'utiliser, exigez-le dans votre test, créez les simulacres, puis transmettez-le à createMockRequire. Par exemple:

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

Et voici un exemple d'un fichier de test complet :

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

si vous voulez faire des tests simples js qui isolent une unité, vous pouvez simplement utiliser cet extrait:

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;
0
user3033599