Wie kann ich Abhängigkeiten für Unit-Tests in RequireJS verspotten?


127

Ich habe ein AMD-Modul, das ich testen möchte, aber ich möchte seine Abhängigkeiten verspotten, anstatt die tatsächlichen Abhängigkeiten zu laden. Ich verwende requirejs und der Code für mein Modul sieht ungefähr so ​​aus:

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

Wie kann ich mich verspotten hurpund durpso effektiv Unit-Tests durchführen?


Ich mache nur ein paar verrückte Eval-Sachen in node.js, um die defineFunktion zu verspotten . Es gibt jedoch einige verschiedene Optionen. Ich werde eine Antwort in der Hoffnung posten, dass es hilfreich sein wird.
Jergason

1
Für Unit-Tests mit Jasmine möchten Sie vielleicht auch einen kurzen Blick auf Jasq werfen . [Haftungsausschluss: Ich halte die
Bibliothek

1
Wenn Sie in Node Env testen, können Sie das Require-Mock- Paket verwenden. Es ermöglicht Ihnen, Ihre Abhängigkeiten leicht zu verspotten, Module zu ersetzen usw. Wenn Sie eine Browser-Umgebung mit asynchronem
Modulladen

Antworten:


64

Nachdem ich diesen Beitrag gelesen hatte, kam ich auf eine Lösung, die die Funktion requirejs config verwendet, um einen neuen Kontext für Ihren Test zu erstellen, in dem Sie einfach Ihre Abhängigkeiten verspotten können:

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

So wird ein neuer Kontext erstellt, in dem die Definitionen für Hurpund Durpvon den Objekten festgelegt werden, die Sie an die Funktion übergeben haben. Das Math.random für den Namen ist vielleicht etwas schmutzig, aber es funktioniert. Wenn Sie eine Reihe von Tests durchführen müssen, müssen Sie für jede Suite einen neuen Kontext erstellen, um die Wiederverwendung Ihrer Mocks zu verhindern, oder Mocks laden, wenn Sie das eigentliche requirejs-Modul benötigen.

In Ihrem Fall würde es so aussehen:

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

Daher verwende ich diesen Ansatz für eine Weile in der Produktion und er ist wirklich robust.


1
Mir gefällt, was Sie hier tun ... zumal Sie für jeden Test einen anderen Kontext laden können. Ich wünschte nur, ich könnte etwas ändern, es scheint, als würde es nur funktionieren, wenn ich alle Abhängigkeiten verspotte. Kennen Sie eine Möglichkeit, die Scheinobjekte zurückzugeben, wenn sie vorhanden sind, aber auf das Abrufen aus der eigentlichen .js-Datei zurückzugreifen, wenn kein Schein bereitgestellt wird? Ich habe versucht, den erforderlichen Code zu durchsuchen, um es herauszufinden, aber ich verliere mich ein wenig.
Glen Hughes

5
Es verspottet nur die Abhängigkeit, die Sie an die createContextFunktion übergeben. Wenn Sie in Ihrem Fall nur {hurp: 'hurp'}an function übergeben, wird die durpDatei als normale Abhängigkeit geladen.
Andreas Köberle

1
Ich verwende dies in Rails (mit jasminerice / phantomjs) und es war die beste Lösung, die ich zum Verspotten mit RequireJS gefunden habe.
Ben Anderson

13
+1 Nicht schön, aber von allen möglichen Lösungen scheint dies die am wenigsten hässliche / unordentliche zu sein. Dieses Problem verdient mehr Aufmerksamkeit.
Chris Salzberg

1
Update: Für alle, die diese Lösung in Betracht ziehen, würde ich empfehlen, die unten genannten squire.js ( github.com/iammerrick/Squire.js ) zu überprüfen . Es ist eine schöne Implementierung einer Lösung, die dieser ähnlich ist und neue Kontexte schafft, wo immer Stubs benötigt werden.
Chris Salzberg

44

Vielleicht möchten Sie sich die neue Squire.js-Bibliothek ansehen

aus den Dokumenten:

Squire.js ist ein Abhängigkeitsinjektor für Require.js-Benutzer, um das Verspotten von Abhängigkeiten zu vereinfachen!


2
Wärmstens empfohlen! Ich aktualisiere meinen Code, um squire.js zu verwenden, und bis jetzt gefällt es mir sehr gut. Sehr, sehr einfacher Code, keine große Magie unter der Haube, aber auf eine (relativ) leicht verständliche Weise.
Chris Salzberg

1
Ich hatte viele Probleme mit der Knappe, die andere Tests beeinflusst, und kann es nicht empfehlen. Ich würde npmjs.com/package/requirejs-mock
Jeff Whiting

17

Ich habe drei verschiedene Lösungen für dieses Problem gefunden, von denen keine angenehm ist.

Abhängigkeiten inline definieren

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

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

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

Fugly. Sie müssen Ihre Tests mit viel AMD Boilerplate überladen.

Laden von Mock-Abhängigkeiten aus verschiedenen Pfaden

Dazu wird eine separate Datei config.js verwendet, um Pfade für jede der Abhängigkeiten zu definieren, die auf Mocks anstelle der ursprünglichen Abhängigkeiten verweisen. Dies ist auch hässlich und erfordert die Erstellung von Tonnen von Testdateien und Konfigurationsdateien.

Fake It In Node

Dies ist meine derzeitige Lösung, aber immer noch eine schreckliche.

Sie erstellen Ihre eigene defineFunktion, um dem Modul eigene Mocks bereitzustellen und Ihre Tests in den Rückruf aufzunehmen. Dann haben Sie evaldas Modul, um Ihre Tests auszuführen, wie folgt:

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

Dies ist meine bevorzugte Lösung. Es sieht ein wenig magisch aus, hat aber einige Vorteile.

  1. Führen Sie Ihre Tests im Knoten aus, damit Sie die Browser-Automatisierung nicht beeinträchtigen.
  2. In Ihren Tests wird weniger AMD-Boilerplate benötigt.
  3. Sie können evalWut einsetzen und sich vorstellen, wie Crockford vor Wut explodiert.

Es hat natürlich immer noch einige Nachteile.

  1. Da Sie in Node testen, können Sie mit Browserereignissen oder DOM-Manipulationen nichts anfangen. Nur gut zum Testen der Logik.
  2. Immer noch etwas umständlich einzurichten. Sie müssen sich definein jedem Test verspotten , da dort Ihre Tests tatsächlich ausgeführt werden.

Ich arbeite an einem Testläufer, um eine schönere Syntax für diese Art von Sachen zu geben, aber ich habe immer noch keine gute Lösung für Problem 1.

Fazit

Das Verspotten von Deps in Requirements ist hart. Ich habe einen Weg gefunden, der irgendwie funktioniert, bin aber immer noch nicht sehr zufrieden damit. Bitte lassen Sie mich wissen, wenn Sie bessere Ideen haben.


15

Es gibt eine config.mapOption http://requirejs.org/docs/api.html#config-map .

Wie man es benutzt:

  1. Normales Modul definieren;
  2. Stub-Modul definieren;
  3. RequireJS genau konfigurieren;

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

In diesem Fall können Sie für Normal- und Testcode das fooModul verwenden, das eine echte Modulreferenz darstellt und entsprechend stub.


Dieser Ansatz hat bei mir sehr gut funktioniert. In meinem Fall habe ich dies dem HTML-Code der Testrunner-Seite hinzugefügt -> map: {'*': {'Common / Modules / nützlicheModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Aligned

9

Sie können testr.js verwenden , um Abhängigkeiten zu verspotten. Sie können testr so einstellen, dass die Scheinabhängigkeiten anstelle der ursprünglichen geladen werden. Hier ist ein Anwendungsbeispiel:

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

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

Überprüfen Sie dies auch: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/


2
Ich wollte wirklich, dass testr.js funktioniert, aber es fühlt sich der Aufgabe noch nicht ganz gewachsen an. Am Ende gehe ich mit der Lösung von @Andreas Köberle, die meinen Tests verschachtelte Kontexte hinzufügt (nicht hübsch), aber konsequent funktioniert. Ich wünschte, jemand könnte sich darauf konzentrieren, diese Lösung auf elegantere Weise zu lösen. Ich werde testr.js weiter beobachten und wenn / wenn es funktioniert, den Wechsel vornehmen.
Chris Salzberg

@shioyama Hallo, danke für das Feedback! Ich würde gerne einen Blick darauf werfen, wie Sie testr.js in Ihrem Teststapel konfiguriert haben. Gerne helfen wir Ihnen bei der Behebung von Problemen! Es gibt auch die Seite Github-Probleme, wenn Sie dort etwas protokollieren möchten. Vielen Dank,
Matty F

1
@MattyF Entschuldigung, ich erinnere mich noch nicht einmal daran, was genau der Grund dafür war, dass testr.js bei mir nicht funktioniert hat, aber ich bin zu dem Schluss gekommen, dass die Verwendung zusätzlicher Kontexte tatsächlich in Ordnung und tatsächlich im Einklang steht mit wie require.js sollte zum verspotten / stubben verwendet werden.
Chris Salzberg

2

Diese Antwort basiert auf der Antwort von Andreas Köberle .
Es war nicht so einfach für mich, seine Lösung zu implementieren und zu verstehen, deshalb werde ich sie etwas detaillierter erklären, wie sie funktioniert, und einige Fallstricke vermeiden, in der Hoffnung, dass sie zukünftigen Besuchern helfen wird.

Also zuerst das Setup:
Ich benutze Karma als Testläufer und MochaJs als Testframework .

Die Verwendung von Squire hat bei mir aus irgendeinem Grund nicht funktioniert. Als ich es verwendete, warf das Testframework Fehler auf:

TypeError: Die Eigenschaft 'call' von undefined kann nicht gelesen werden

RequireJs bietet die Möglichkeit, Modul-IDs anderen Modul-IDs zuzuordnen . Außerdem können Sie eine requireFunktion erstellen , die eine andere Konfiguration als die globale verwendet require.
Diese Funktionen sind für das Funktionieren dieser Lösung von entscheidender Bedeutung.

Hier ist meine Version des Scheincodes, einschließlich (vieler) Kommentare (ich hoffe, es ist verständlich). Ich habe es in ein Modul eingewickelt, damit die Tests es leicht erfordern können.

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

Die größte Gefahr, auf die ich gestoßen bin und die mich buchstäblich Stunden gekostet hat, war das Erstellen der RequireJs-Konfiguration. Ich habe versucht, es (tief) zu kopieren und nur die erforderlichen Eigenschaften (wie Kontext oder Karte) zu überschreiben. Das funktioniert nicht! Kopieren Sie nur die baseUrl, das funktioniert gut.

Verwendung

Um es zu verwenden, benötigen Sie es in Ihrem Test, erstellen Sie die Mocks und übergeben Sie es dann an createMockRequire. Beispielsweise:

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

Und hier ein Beispiel für eine vollständige Testdatei :

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

Wenn Sie einige einfache JS-Tests durchführen möchten, die eine Einheit isolieren, können Sie einfach dieses Snippet verwenden:

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;
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.