Wie verspotte ich die Importe eines ES6-Moduls?

141

Ich habe die folgenden ES6-Module:

network.js

export function getDataFromServer() {
  return ...
}

widget.js

import { getDataFromServer } from 'network.js';

export class Widget() {
  constructor() {
    getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }

  render() {
    ...
  }
}

Ich suche nach einer Möglichkeit, Widget mit einer Scheininstanz von zu testen getDataFromServer. Wenn ich <script>wie in Karma separate s anstelle von ES6-Modulen verwenden würde, könnte ich meinen Test wie folgt schreiben:

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Wenn ich jedoch ES6-Module einzeln außerhalb eines Browsers teste (wie bei Mocha + babel), würde ich Folgendes schreiben:

import { Widget } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(?????) // How to mock?
    .andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Okay, aber jetzt getDataFromServerist es nicht verfügbar window(nun, es gibt überhaupt keine window), und ich kenne keine Möglichkeit, Dinge direkt in widget.jsden eigenen Bereich zu injizieren .

Wohin gehe ich von hier aus?

  1. Gibt es eine Möglichkeit, auf den Umfang von zuzugreifen widget.jsoder zumindest seine Importe durch meinen eigenen Code zu ersetzen?
  2. Wenn nicht, wie kann ich Widgettestbar machen ?

Sachen, über die ich nachgedacht habe:

ein. Manuelle Abhängigkeitsinjektion.

Entfernen Sie alle Importe von widget.jsund erwarten Sie, dass der Anrufer die Deps bereitstellt.

export class Widget() {
  constructor(deps) {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

Es ist mir sehr unangenehm, die öffentliche Oberfläche von Widget so durcheinander zu bringen und Implementierungsdetails offenzulegen. No Go.


b. Legen Sie die Importe offen, um sie verspotten zu können.

Etwas wie:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

export class Widget() {
  constructor() {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

dann:

import { Widget, deps } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(deps.getDataFromServer)  // !
      .andReturn("mockData");
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Dies ist weniger invasiv, erfordert jedoch, dass ich für jedes Modul eine Menge Boilerplate schreibe, und es besteht immer noch das Risiko, dass ich es getDataFromServeranstelle von deps.getDataFromServerständig verwende. Ich bin mir nicht sicher, aber das ist meine bisher beste Idee.

Kos
quelle
Wenn es keine native Mock-Unterstützung für diese Art von Import gibt, würde ich wahrscheinlich darüber nachdenken, einen eigenen Transformator für die Konvertierung Ihres ES6-Imports in ein benutzerdefiniertes Mockable-Importsystem zu schreiben. Dies würde mit Sicherheit eine weitere Ebene möglicher Fehler hinzufügen und den zu testenden Code ändern, ....
t.niese
Ich kann derzeit keine Testsuite festlegen, aber ich würde versuchen, die Funktion von jasmin createSpy( github.com/jasmine/jasmine/blob/… ) mit einem importierten Verweis auf getDataFromServer aus dem Modul 'network.js' zu verwenden. Damit Sie in der Testdatei des Widgets getDataFromServer importieren und dannlet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed
Die zweite Vermutung besteht darin, ein Objekt aus dem Modul 'network.js' zurückzugeben, keine Funktion. Auf diese Weise können Sie spyOnauf diesem Objekt aus dem network.jsModul importieren . Es ist immer ein Verweis auf dasselbe Objekt.
Microfed
Eigentlich ist es schon ein Objekt, soweit
Microfed
2
Ich verstehe nicht wirklich, wie die Abhängigkeitsinjektion die Widgetöffentliche Schnittstelle durcheinander bringt . Widgetist durcheinander ohne deps . Warum nicht die Abhängigkeit explizit machen?
Thebearingedge

Antworten:

129

Ich habe begonnen, den import * as objStil in meinen Tests zu verwenden, der alle Exporte aus einem Modul als Eigenschaften eines Objekts importiert, das dann verspottet werden kann. Ich finde das viel sauberer als die Verwendung von Rewire oder Proxyquire oder einer ähnlichen Technik. Ich habe dies am häufigsten getan, wenn ich beispielsweise Redux-Aktionen verspotten musste. Folgendes könnte ich für Ihr Beispiel oben verwenden:

import * as network from 'network.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Wenn Ihre Funktion zufällig ein Standardexport ist, import * as network from './network'würde dies produzieren {default: getDataFromServer}und Sie können network.default verspotten.

Carpeliam
quelle
3
Verwenden Sie die import * as objnur im Test oder auch in Ihrem regulären Code?
Chau Thai
35
@carpeliam Dies funktioniert nicht mit der ES6-Modulspezifikation, bei der die Importe schreibgeschützt sind.
Ashish
7
Jasmine beschwert sich [method_name] is not declared writable or has no setter, was Sinn macht, da die es6-Importe konstant sind. Gibt es eine Möglichkeit, dies zu umgehen?
lpan
2
@Francisc import(im Gegensatz zu require, das überall hingehen kann) wird gehisst, sodass Sie technisch nicht mehrmals importieren können. Klingt so, als würde Ihr Spion woanders angerufen? Um zu verhindern, dass die Tests durcheinander geraten (als Testverschmutzung bezeichnet), können Sie Ihre Spione in einem afterEach (z. B. sinon.sandbox) zurücksetzen. Ich glaube, Jasmine macht das automatisch.
Carpeliam
10
@ agent47 Das Problem ist, dass die ES6-Spezifikation zwar ausdrücklich verhindert, dass diese Antwort funktioniert, aber genau so, wie Sie es erwähnt haben, die meisten Leute, die importin ihrem JS schreiben , ES6-Module nicht wirklich verwenden. So etwas wie Webpack oder Babel wird beim Erstellen eingreifen und es entweder in einen eigenen internen Mechanismus zum Aufrufen entfernter Teile des Codes (z. B. __webpack_require__) oder in einen der De-facto- Standards vor ES6 , CommonJS, AMD oder UMD, konvertieren. Und diese Konvertierung hält sich oft nicht strikt an die Spezifikation. Für viele, viele Entwickler funktioniert diese Antwort jetzt einwandfrei. Zur Zeit.
Daemonexmachina
30

@carpeliam ist korrekt, aber beachten Sie, dass Sie diese Funktion als Teil des Export-Namespace aufrufen müssen, wenn Sie eine Funktion in einem Modul ausspionieren und eine andere Funktion in diesem Modul verwenden möchten, die diese Funktion aufruft. Andernfalls wird der Spion nicht verwendet.

Falsches Beispiel:

// mymodule.js

export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will still be 2
    });
});

Richtiges Beispiel:

export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will be 3 which is what you expect
    });
});
vdloo
quelle
4
Ich wünschte, ich könnte diese Antwort noch 20 Mal abstimmen! Danke dir!
Sfletche
Kann jemand erklären, warum dies der Fall ist? Ist exports.myfunc2 () eine Kopie von myfunc2 () ohne direkte Referenz?
Colin Whitmarsh
2
@ColinWhitmarsh exports.myfunc2ist ein direkter Verweis auf, myfunc2bis spyOnes durch einen Verweis auf eine Spionagefunktion ersetzt wird. spyOnwird den Wert von ändern exports.myfunc2und durch ein Spionageobjekt ersetzen, während es myfunc2im Bereich des Moduls unberührt bleibt (weil spyOnes keinen Zugriff darauf hat)
madprog
*Sollte das Objekt nicht mit Freeze importiert werden und die Objektattribute können nicht geändert werden?
Agent47
1
Nur ein Hinweis, dass diese Empfehlung, export functionzusammen mit zu verwenden exports.myfunc2, Commonjs und ES6-Modulsyntax technisch mischt und dies in neueren Versionen von Webpack (2+) nicht zulässig ist, die die Verwendung der ES6-Modulsyntax alles oder nichts erfordern. Ich habe unten eine Antwort hinzugefügt, die auf dieser basiert und in strengen ES6-Umgebungen funktioniert.
QuarkleMotion
6

Ich habe eine Bibliothek implementiert, die versucht, das Problem der Laufzeitverspottung von Typescript-Klassenimporten zu lösen, ohne dass die ursprüngliche Klasse über explizite Abhängigkeitsinjektionen informiert sein muss.

Die Bibliothek verwendet die import * as Syntax und ersetzt dann das ursprünglich exportierte Objekt durch eine Stub-Klasse. Die Typensicherheit bleibt erhalten, sodass Ihre Tests zur Kompilierungszeit unterbrochen werden, wenn ein Methodenname aktualisiert wurde, ohne den entsprechenden Test zu aktualisieren.

Diese Bibliothek finden Sie hier: ts-mock-imports .

EmandM
quelle
1
Dieses Modul benötigt mehr Github-Sterne
SD
6

Die Antwort von @ vdloo brachte mich in die richtige Richtung, aber die gemeinsamen Verwendung der Schlüsselwörter "export" und ES6-Modul "export" in derselben Datei funktionierte nicht für mich (Webpack v2 oder höher beschwert sich). Stattdessen verwende ich einen Standardexport (benannte Variable), der alle Exporte einzelner benannter Module umschließt und dann den Standardexport in meine Testdatei importiert. Ich verwende das folgende Export-Setup mit Mokka / Sinon und Stubbing funktioniert einwandfrei, ohne dass eine Neuverdrahtung erforderlich ist usw.:

// MyModule.js
let MyModule;

export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }

export default MyModule = {
  myfunc1,
  myfunc2
}

// tests.js
import MyModule from './MyModule'

describe('MyModule', () => {
  const sandbox = sinon.sandbox.create();
  beforeEach(() => {
    sandbox.stub(MyModule, 'myfunc2').returns(4);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('myfunc1 is a proxy for myfunc2', () => {
    expect(MyModule.myfunc1()).to.eql(4);
  });
});
QuarkleMotion
quelle
Hilfreiche Antwort, danke. Ich wollte nur erwähnen, dass das let MyModulenicht erforderlich ist, um den Standardexport zu verwenden (es kann ein Rohobjekt sein). Außerdem muss diese Methode nicht myfunc1()aufgerufen werden myfunc2(), sondern funktioniert nur, um sie direkt auszuspionieren.
Mark Edington
@QuarkleMotion: Es scheint, dass Sie dies versehentlich mit einem anderen Konto als Ihrem Hauptkonto bearbeitet haben. Aus diesem Grund musste Ihre Bearbeitung eine manuelle Genehmigung durchlaufen - es sah nicht so aus, als wäre es von Ihnen. Ich nehme an, dies war nur ein Unfall, aber wenn es beabsichtigt war, sollten Sie die offiziellen Richtlinien für Sockenpuppenkonten lesen, damit Sie Verstößt nicht versehentlich gegen die Regeln .
Auffälliger Compiler
1
@ConspicuousCompiler, danke für das Heads-up - dies war ein Fehler. Ich hatte nicht vor, diese Antwort mit meinem E-Mail-verknüpften SO-Konto zu ändern.
QuarkleMotion
Dies scheint eine Antwort auf eine andere Frage zu sein! Wo ist widget.js und network.js? Diese Antwort scheint keine transitive Abhängigkeit zu haben, was die ursprüngliche Frage schwierig machte.
Bennett McElwee
3

Ich habe festgestellt, dass diese Syntax funktioniert:

Mein Modul:

// mymod.js
import shortid from 'shortid';

const myfunc = () => shortid();
export default myfunc;

Testcode meines Moduls:

// mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';

jest.mock('shortid');

describe('mocks shortid', () => {
  it('works', () => {
    shortid.mockImplementation(() => 1);
    expect(myfunc()).toEqual(1);
  });
});

Siehe das Dokument .

Nerfologe
quelle
+1 und mit einigen zusätzlichen Anweisungen: Scheint nur mit Knotenmodulen zu funktionieren, dh mit Sachen, die Sie auf package.json haben. Und was noch wichtiger ist, etwas, das in den Jest-Dokumenten nicht erwähnt wird, muss die übergebene Zeichenfolge jest.mock()mit dem Namen übereinstimmen, der in import / packge.json anstelle des Namens der Konstante verwendet wird. In den Dokumenten sind beide gleich, aber mit Code wie import jwt from 'jsonwebtoken'Sie müssen Sie den Mock alsjest.mock('jsonwebtoken')
kaskelotti
0

Ich habe es nicht selbst versucht, aber ich denke, Spott könnte funktionieren. Sie können das reale Modul durch ein von Ihnen bereitgestelltes Modell ersetzen. Im Folgenden finden Sie ein Beispiel, das Ihnen eine Vorstellung davon gibt, wie es funktioniert:

mockery.enable();
var networkMock = {
    getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);

import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'

mockery.deregisterMock('network.js');
mockery.disable();

Es scheint, als würde mockeryes nicht mehr gewartet und ich denke, es funktioniert nur mit Node.js, aber es ist trotzdem eine nette Lösung, um Module zu verspotten, die sonst schwer zu verspotten sind.

Erik B.
quelle