Jak udawać importy modułu ES6?

141

Mam następujące moduły ES6:

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() {
    ...
  }
}

Szukam sposobu na przetestowanie Widget za pomocą próbnej instancji getDataFromServer. Gdybym użył oddzielnych <script>modułów zamiast modułów ES6, jak w Karmie, mógłbym napisać swój test tak:

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

Jeśli jednak testuję moduły ES6 indywidualnie poza przeglądarką (jak z Mocha + babel), napisałbym coś takiego:

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

OK, ale teraz getDataFromServernie jest dostępny w window(cóż, w ogóle nie ma window) i nie znam sposobu, aby wstrzyknąć rzeczy bezpośrednio do widget.jswłasnego zakresu.

Więc dokąd mam się udać?

  1. Czy istnieje sposób uzyskania dostępu do zakresu widget.jslub przynajmniej zastąpienia jego importu własnym kodem?
  2. Jeśli nie, w jaki sposób mogę uczynić Widgetmożliwym do przetestowania?

Rzeczy, które rozważałem:

za. Ręczne wstrzykiwanie zależności.

Usuń wszystkie importy z widget.jsi oczekuj, że wywołujący dostarczy deps.

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

Nie podoba mi się zepsucie publicznego interfejsu Widgeta w ten sposób i ujawnianie szczegółów implementacji. Nie idź.


b. Ujawnij importy, aby umożliwić ich kpienie.

Coś jak:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

następnie:

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

Jest to mniej inwazyjne, ale wymaga ode mnie napisania wielu schematów dla każdego modułu i nadal istnieje ryzyko, że będę go używać getDataFromServerzamiast tego przez deps.getDataFromServercały czas. Niepokoi mnie to, ale jak dotąd to mój najlepszy pomysł.

Kos
źródło
Jeśli nie ma natywnej obsługi makiety dla tego rodzaju importu, prawdopodobnie zastanowiłbym się nad napisaniem własnego transformatora do babel, konwertującego import w stylu ES6 na niestandardowy, symulowany system importu. To z pewnością dodałoby kolejną warstwę możliwej awarii i zmieniłoby kod, który chcesz przetestować, ....
t.niese
Nie mogę teraz ustawić zestawu testów, ale spróbuję użyć funkcji Jasmin createSpy( github.com/jasmine/jasmine/blob/… ) z importowanym odniesieniem do getDataFromServer z modułu „network.js”. Tak więc w pliku testów widżetu zaimportowałbyś getDataFromServer, a następnielet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed,
Drugie przypuszczenie to zwrócenie obiektu z modułu „network.js”, a nie funkcji. W ten sposób możesz spyOnna tym obiekcie zaimportowanym z network.jsmodułu. Jest to zawsze odniesienie do tego samego obiektu.
Mikrofonowanie
Właściwie to już obiekt, z tego co widzę: babeljs.io/repl/…
Microfed
2
Naprawdę nie rozumiem, w jaki sposób iniekcja zależności psuje Widgetpubliczny interfejs? Widgetjest pomieszany bez deps . Dlaczego nie wyrazić zależności w sposób jawny?
thebearingedge

Odpowiedzi:

129

Zacząłem stosować import * as objstyl w moich testach, który importuje wszystkie eksporty z modułu jako właściwości obiektu, z którego można następnie mockować. Uważam, że jest to o wiele czystsze niż użycie czegoś takiego jak rewire lub proxyquire lub inna podobna technika. Robiłem to najczęściej, gdy potrzebowałem na przykład kpić z akcji Redux. Oto, czego mógłbym użyć w powyższym przykładzie:

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

Jeśli twoja funkcja jest domyślnym eksportem, to import * as network from './network'utworzy {default: getDataFromServer}i możesz mockować network.default.

carpeliam
źródło
3
Używasz import * as objjedynego w teście, czy też w swoim zwykłym kodzie?
Chau Thai,
36
@carpeliam To nie zadziała ze specyfikacją modułu ES6, gdzie import jest tylko do odczytu.
ashish
7
Jasmine narzeka, [method_name] is not declared writable or has no setterco ma sens, ponieważ import es6 jest stały. Czy istnieje sposób obejścia tego problemu?
lpan
2
@Francisc import(w przeciwieństwie do tego require, który może dotrzeć wszędzie) jest podnoszony, więc nie można technicznie importować wiele razy. Wygląda na to, że ktoś dzwoni do twojego szpiega? Aby zapobiec zepsuciu testów (tzw. Testowym zanieczyszczeniom), możesz zresetować swoich szpiegów w afterEach (np. Sinon.sandbox). Uważam, że Jasmine robi to automatycznie.
carpeliam
10
@ agent47 Problem polega na tym, że chociaż specyfikacja ES6 konkretnie uniemożliwia działanie tej odpowiedzi, dokładnie tak, jak wspomniałeś, większość ludzi piszących importw swoim JS nie używa tak naprawdę modułów ES6. Coś takiego jak webpack lub babel wkracza w czasie kompilacji i konwertuje je albo na swój własny wewnętrzny mechanizm do wywoływania odległych części kodu (np. __webpack_require__), Albo w jeden z de facto standardów sprzed ES6 , CommonJS, AMD lub UMD. A ta konwersja często nie jest ściśle zgodna ze specyfikacją. Tak więc dla wielu, wielu programistów w tej chwili ta odpowiedź działa dobrze. Na razie.
daemonexmachina
31

@carpeliam jest poprawne, ale pamiętaj, że jeśli chcesz szpiegować funkcję w module i użyć innej funkcji w tym module wywołującej tę funkcję, musisz wywołać tę funkcję jako część przestrzeni nazw eksportu, w przeciwnym razie szpieg nie będzie używany.

Błędny przykład:

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

Właściwy przykład:

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
źródło
4
Chciałbym móc zagłosować na tę odpowiedź jeszcze 20 razy! Dziękuję Ci!
sfletche
Czy ktoś może wyjaśnić, dlaczego tak jest? Czy exports.myfunc2 () jest kopią myfunc2 () bez bezpośredniego odniesienia?
Colin Whitmarsh
2
@ColinWhitmarsh exports.myfunc2jest bezpośrednim odniesieniem do myfunc2do momentu, w którym spyOnzastępuje go odniesieniem do funkcji szpiegowskiej. spyOnzmieni wartość exports.myfunc2i zastąpi go obiektem szpiegowskim, myfunc2pozostając nietkniętym w zakresie modułu (ponieważ spyOnnie ma do niego dostępu)
madprog
nie powinno importować z *zamrożeniem obiektu, a atrybuty obiektu nie mogą być zmienione?
agent47
1
Tylko uwaga, że ​​ta rekomendacja używania export functionrazem z exports.myfunc2technicznie polega na mieszaniu składni modułu commonjs i ES6 i jest to niedozwolone w nowszych wersjach pakietu webpack (2+), które wymagają użycia składni modułu ES6 typu wszystko albo nic. Poniżej dodałem odpowiedź w oparciu o tę, która będzie działać w ścisłych środowiskach ES6.
QuarkleMotion
6

Zaimplementowałem bibliotekę, która próbuje rozwiązać problem mockowania w czasie wykonywania importu klas Typescript bez potrzeby, aby oryginalna klasa wiedziała o jakimkolwiek jawnym wstrzyknięciu zależności.

Biblioteka używa import * asskładni, a następnie zastępuje oryginalny wyeksportowany obiekt klasą pośredniczącą. Zachowuje bezpieczeństwo typów, więc testy zostaną przerwane w czasie kompilacji, jeśli nazwa metody została zaktualizowana bez aktualizacji odpowiedniego testu.

Tę bibliotekę można znaleźć tutaj: ts-mock-Import .

EmandM
źródło
1
Ten moduł potrzebuje więcej gwiazdek github
SD
6

Odpowiedź @ vdloo sprawiła, że ​​poszedłem we właściwym kierunku, ale używanie razem słów kluczowych commonjs „export” i „export” modułu ES6 w tym samym pliku nie zadziałało (narzeka webpack v2 lub nowszy). Zamiast tego używam domyślnego (nazwanej zmiennej) eksportu opakowującego wszystkie poszczególne eksporty z nazwanego modułu, a następnie importuję domyślny eksport w moim pliku testów. Używam następującej konfiguracji eksportu z mokką / sinonem, a stubbing działa dobrze bez konieczności ponownego okablowania itp .:

// 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
źródło
Pomocna odpowiedź, dzięki. Chciałem tylko wspomnieć, że let MyModulenie jest wymagane użycie domyślnego eksportu (może to być surowy obiekt). Ponadto ta metoda nie wymaga myfunc1()dzwonienia myfunc2(), działa tylko po to, aby ją bezpośrednio szpiegować.
Mark Edington,
@QuarkleMotion: Wygląda na to, że przez przypadek edytowałeś to na innym koncie niż na koncie głównym. Dlatego twoja edycja musiała przejść przez ręczne zatwierdzenie - nie wyglądało na to, że pochodziła od ciebie , zakładam, że to był tylko wypadek, ale jeśli był celowy, powinieneś przeczytać oficjalne zasady dotyczące kont lalek skarpetkowych, więc nie naruszaj przypadkowo zasad .
Conspicuous Compiler
1
@ConspicuousCompiler dzięki za ostrzeżenie - to był błąd, nie zamierzałem modyfikować tej odpowiedzi za pomocą mojego służbowego konta SO połączonego z e-mailem.
QuarkleMotion
To wydaje się być odpowiedzią na inne pytanie! Gdzie jest widget.js i network.js? Wydaje się, że ta odpowiedź nie ma zależności przechodnich, co utrudniało pierwotne pytanie.
Bennett McElwee
3

Zauważyłem, że ta składnia działa:

Mój moduł:

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

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

Kod testowy mojego modułu:

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

Zobacz doc .

nerfolog
źródło
+1 i dodatkowe instrukcje: Wydaje się, że działa tylko z modułami węzłów, tj. Z rzeczami, które masz na package.json. Co ważniejsze, coś, o czym nie ma wzmianki w dokumentach Jest, przekazywany ciąg jest.mock()musi odpowiadać nazwie użytej w import / packge.json zamiast nazwy stałej. W dokumentach oba są takie same, ale z kodem jak import jwt from 'jsonwebtoken'trzeba ustawić makietę jakjest.mock('jsonwebtoken')
kaskelotti
0

Sam tego nie próbowałem, ale myślę, że kpina może działać. Pozwala na zastąpienie prawdziwego modułu dostarczonym przez Ciebie makietą. Poniżej znajduje się przykład, który daje wyobrażenie o tym, jak to działa:

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

Wygląda na mockeryto, że nie jest już obsługiwany i myślę, że działa tylko z Node.js, ale nie mniej jednak jest to zgrabne rozwiązanie do wyszydzania modułów, które inaczej są trudne do podrobienia.

Erik B.
źródło