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

127

J'ai un module AMD que je veux tester, 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 me moquer hurpet durpainsi pouvoir effectuer efficacement un test unitaire?

Jergason
la source
Je fais juste des trucs fous eval dans node.js pour simuler la definefonction. Il existe cependant quelques options différentes. Je posterai une réponse dans l'espoir qu'elle vous sera utile.
jergason
1
Pour les tests unitaires avec Jasmine, vous pouvez également jeter un coup d'œil à Jasq . [Clause de non-responsabilité: je maintiens la
bibliothèque
1
Si vous testez dans node env, vous pouvez utiliser le package require-mock . Il vous permet de moquez facilement vos dépendances, remplacer les modules , etc. Si vous avez besoin d' env navigateur avec charge du module async - vous pouvez essayer Squire.js
ValeriiVasin

Réponses:

64

Donc, après avoir lu cet article, j'ai trouvé 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 pour Hurpet Durpseront définies par les objets que vous avez transmis à la fonction. Le Math.random pour le nom est peut-être un peu sale mais cela fonctionne. 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 vrai module requirejs.

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

J'utilise donc cette approche en production depuis un moment et c'est vraiment robuste.

Andreas Köberle
la source
1
J'aime ce que vous faites ici ... d'autant plus que vous pouvez charger un contexte différent pour chaque test. La seule chose que j'aimerais pouvoir changer, c'est qu'il semble que cela ne fonctionne que si je me moque de toutes les dépendances. Connaissez-vous un moyen de renvoyer les objets fictifs s'ils sont là, mais de revenir à la récupération à partir du fichier .js réel si aucune simulation n'est fournie? J'ai essayé de fouiller dans le code requis pour le comprendre, mais je me perds un peu.
Glen Hughes
5
Il se moque uniquement de la dépendance que vous transmettez à la createContextfonction. Donc, dans votre cas, si vous passez uniquement {hurp: 'hurp'}à la fonction, le durpfichier sera chargé comme une dépendance normale.
Andreas Köberle
1
J'utilise ceci dans Rails (avec jasminerice / phantomjs) et c'est la meilleure solution que j'ai trouvée pour me moquer de RequireJS.
Ben Anderson
13
+1 Pas joli, mais de toutes les solutions possibles, celle-ci semble être la moins moche / désordonnée. Ce problème mérite plus d'attention.
Chris Salzberg
1
Mise à jour: à tous ceux qui envisagent cette solution, je suggérerais de consulter squire.js ( github.com/iammerrick/Squire.js ) mentionné ci-dessous. C'est une belle implémentation d'une solution similaire à celle-ci, créant de nouveaux contextes partout où des stubs sont nécessaires.
Chris Salzberg
44

vous voudrez peut-être consulter la nouvelle lib Squire.js

à partir de la documentation:

Squire.js est un injecteur de dépendances pour les utilisateurs de Require.js afin de rendre les dépendances simulées facilement!

bustiqué
la source
2
Fortement recommandé! Je mets à jour mon code pour utiliser squire.js et jusqu'à présent je l'aime beaucoup. Code très très simple, pas de grande magie sous le capot, mais fait d'une manière (relativement) facile à comprendre.
Chris Salzberg
1
J'ai eu beaucoup de problèmes avec le côté squire lors d'autres tests et je ne peux pas le recommander. Je recommanderais npmjs.com/package/requirejs-mock
Jeff Whiting
17

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

Définition des 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 beaucoup de passe-partout AMD.

Chargement de dépendances simulées à partir de différents chemins

Cela implique l'utilisation d'un fichier config.js distinct pour définir des chemins pour chacune des dépendances qui pointent vers des simulations 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

C'est ma solution actuelle, mais elle est toujours terrible.

Vous créez votre propre definefonction pour fournir vos propres simulations au module et mettre vos tests dans le callback. Ensuite, vous evalle 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);

C'est ma solution préférée. Cela a l'air un peu magique, mais cela présente quelques avantages.

  1. Exécutez vos tests dans node, donc pas de problème avec l'automatisation du navigateur.
  2. Moins besoin de passe-partout AMD désordonné dans vos tests.
  3. Vous vous en servez evaldans la colère et imaginez Crockford exploser de rage.

Il a encore des inconvénients, évidemment.

  1. Puisque vous testez dans node, vous ne pouvez rien faire avec les événements du navigateur ou la manipulation DOM. Seulement bon pour tester la logique.
  2. Encore un peu maladroit à mettre en place. Vous devez vous moquer definede chaque test, car c'est là que vos tests s'exécutent réellement.

Je travaille sur un testeur pour donner une meilleure syntaxe pour ce genre de choses, mais je n'ai toujours pas de bonne solution pour le problème 1.

Conclusion

Mocking deps dans requirejs est nul. J'ai trouvé un moyen qui fonctionne en quelque sorte, mais je ne suis toujours pas très satisfait. Veuillez me faire savoir si vous avez de meilleures idées.

Jergason
la source
15

Il existe une config.mapoption http://requirejs.org/docs/api.html#config-map .

Comment l'utiliser:

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

    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 foomodule qui sera la véritable référence du module et le stub en conséquence.

Artem Oboturov
la source
Cette approche a très bien fonctionné pour moi. Dans mon cas, j'ai ajouté ceci au html de la page de test runner -> map: {'*': {'Common / Modules / usefulModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Aligned
9

Vous pouvez utiliser testr.js pour simuler des dépendances. Vous pouvez configurer testr pour charger les dépendances fictives 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
});

Vérifiez également ceci: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

Janith
la source
2
Je voulais vraiment que testr.js fonctionne, mais cela ne me semble pas encore tout à fait à la hauteur de la tâche. En fin de compte, je vais avec la solution de @Andreas Köberle, qui ajoutera des contextes imbriqués à mes tests (pas joli) mais qui fonctionne toujours. Je souhaite que quelqu'un puisse se concentrer sur la résolution de cette solution d'une manière plus élégante. Je continuerai à regarder testr.js et si / quand cela fonctionne, je ferai le changement.
Chris Salzberg
@shioyama salut, merci pour les commentaires! J'aimerais voir comment vous avez configuré testr.js dans votre pile de test. Heureux de vous aider à résoudre tous les problèmes que vous pourriez rencontrer! Il y a aussi la page Problèmes de github si vous souhaitez y enregistrer quelque chose. Merci,
Matty F
1
@MattyF désolé, je ne me souviens même pas pour le moment de la raison exacte pour laquelle testr.js n'a pas fonctionné pour moi, mais j'en suis venu à la conclusion que l'utilisation de contextes supplémentaires est en fait très bien et en fait conforme avec comment require.js était destiné à être utilisé pour moquer / stubbing.
Chris Salzberg
2

Cette réponse est basée sur la réponse d'Andreas Köberle .
Ce n'était pas si simple pour moi d'implémenter et de comprendre sa solution, je vais donc vous expliquer un peu plus en détail comment cela fonctionne, et quelques écueils à éviter, en espérant que cela aidera les futurs visiteurs.

Donc, tout d'abord la configuration:
j'utilise Karma comme testeur et MochaJs comme cadre de test.

Utiliser quelque chose comme Squire ne fonctionnait pas 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é 'call' de undefined

RequireJs a la possibilité de mapper les identifiants de module à d'autres identifiants de module. Il permet également de créer une requirefonction qui utilise une configuration différente de celle du global require.
Ces fonctionnalités sont essentielles pour que cette solution fonctionne.

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

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é des heures, a été la création de la configuration RequireJs. J'ai essayé de le copier (en profondeur) et de ne remplacer que les propriétés nécessaires (comme le contexte ou la carte). Cela ne fonctionne pas! Copiez uniquement lebaseUrl , cela fonctionne très 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 de 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);
                        }
                    });
                });
            });
        });
    });
});
Domysee
la source
0

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

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;
user3033599
la source