Come deridere le importazioni di un modulo ES6?


142

Ho i seguenti moduli 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() {
    ...
  }
}

Sto cercando un modo per testare Widget con una finta istanza di getDataFromServer. Se avessi usato <script>moduli separati anziché ES6, come in Karma, avrei potuto scrivere il mio test come:

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

Tuttavia, se sto testando i moduli ES6 singolarmente al di fuori di un browser (come con Mocha + babel), scriverei qualcosa del tipo:

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

Va bene, ma ora getDataFromServernon è disponibile in window(beh, non c'è windowaffatto), e non conosco un modo per iniettare cose direttamente nel widget.jsproprio ambito.

Quindi dove vado da qui?

  1. Esiste un modo per accedere all'ambito widget.jso almeno sostituire le sue importazioni con il mio codice?
  2. In caso contrario, come posso renderlo Widgettestabile?

Roba che ho considerato:

un. Iniezione manuale delle dipendenze.

Rimuovere tutte le importazioni da widget.jse aspettarsi che il chiamante fornisca i deps.

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

Sono molto a disagio nel rovinare l'interfaccia pubblica di Widget in questo modo e nel mostrare i dettagli dell'implementazione. Non andare.


b. Esporre le importazioni per consentire di deriderle.

Qualcosa di simile a:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

poi:

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

Questo è meno invasivo ma mi richiede di scrivere un sacco di boilerplate per ogni modulo, e c'è comunque il rischio che io lo usi getDataFromServerinvece di deps.getDataFromServertutto il tempo. Non sono a mio agio, ma finora è la mia migliore idea.


Se non esiste un supporto mock nativo per questo tipo di importazione, probabilmente penserei a scrivere un proprio trasformatore per babel che converta l'importazione in stile ES6 in un sistema di importazione mockable personalizzato. Questo sicuramente aggiungerebbe un altro livello di possibile errore e cambierebbe il codice che si desidera verificare, ....
t.niese,

Non posso impostare una suite di test in questo momento, ma proverei a usare la funzione di jasmin createSpy( github.com/jasmine/jasmine/blob/… ) con un riferimento importato per ottenereDataFromServer dal modulo 'network.js'. In modo che, nel file di test del widget, importassi getDataFromServer e poilet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed,

La seconda ipotesi è quella di restituire un oggetto dal modulo 'network.js', non una funzione. In questo modo, potresti spyOnsu quell'oggetto, importato dal network.jsmodulo. È sempre un riferimento allo stesso oggetto.
Microfed

In realtà, è già un oggetto, da quello che posso vedere: babeljs.io/repl/…
Microfed

2
Non capisco davvero come l'iniezione di dipendenza rovini Widgetl'interfaccia pubblica? Widgetè incasinato senza deps . Perché non rendere esplicita la dipendenza?
thebearingedge,

Risposte:


130

Ho iniziato a utilizzare lo import * as objstile nei miei test, che importa tutte le esportazioni da un modulo come proprietà di un oggetto che può quindi essere deriso. Trovo che sia molto più pulito rispetto all'utilizzo di qualcosa come rewire o proxyquire o qualsiasi tecnica simile. L'ho fatto più spesso quando ho bisogno di deridere azioni Redux, per esempio. Ecco cosa potrei usare per il tuo esempio sopra:

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

Se la tua funzione risulta essere un'esportazione predefinita, allora import * as network from './network'produrrebbe {default: getDataFromServer}e puoi deridere network.default.


3
Usi l' import * as objunico nel test o anche nel tuo codice normale?
Chau Thai,

37
@carpeliam Questo non funzionerà con le specifiche del modulo ES6 in cui le importazioni sono di sola lettura.
ashish,

7
Jasmine si lamenta, il [method_name] is not declared writable or has no setterche ha senso dato che le importazioni di es6 sono costanti. C'è un modo per aggirare il problema?
lpan,

2
@Francisc import(a differenza di require, che può andare ovunque) viene sollevato, quindi non è possibile importare tecnicamente più volte. Sembra che la tua spia venga chiamata altrove? Al fine di mantenere i test dallo stato di confusione (noto come test di inquinamento), è possibile ripristinare le spie in un AfterEach (ad esempio sinon.sandbox). Jasmine Credo che lo faccia automaticamente.
carpeliam,

11
@ agent47 Il problema è che mentre le specifiche ES6 impediscono in modo specifico che questa risposta funzioni, esattamente come hai menzionato, la maggior parte delle persone che scrivono importnel proprio JS non utilizzano realmente i moduli ES6. Qualcosa come webpack o babel interverrà in fase di build e lo convertirà nel proprio meccanismo interno per chiamare parti distanti del codice (ad esempio __webpack_require__) o in uno degli standard di fatto pre-ES6 , CommonJS, AMD o UMD. E quella conversione spesso non aderisce strettamente alle specifiche. Quindi per molti, molti sviluppatori proprio ora, questa risposta funziona bene. Per adesso.
daemonexmachina,

31

@carpeliam è corretto ma si noti che se si desidera spiare una funzione in un modulo e utilizzare un'altra funzione in quel modulo che chiama quella funzione, è necessario chiamare quella funzione come parte dello spazio dei nomi delle esportazioni, altrimenti la spia non verrà utilizzata.

Esempio sbagliato:

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

Giusto esempio:

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

4
Vorrei poter votare questa risposta altre 20 volte! Grazie!
sfletche,

Qualcuno può spiegare perché questo è il caso? Exports.myfunc2 () è una copia di myfunc2 () senza essere un riferimento diretto?
Colin Whitmarsh,

2
@ColinWhitmarsh exports.myfunc2è un riferimento diretto myfunc2fino a quando spyOnnon lo sostituisce con un riferimento a una funzione spia. spyOncambierà il valore di exports.myfunc2e lo sostituirà con un oggetto spia, mentre myfunc2rimane intatto nell'ambito del modulo (perché spyOnnon ha accesso ad esso)
madprog,

l'importazione non dovrebbe essere bloccata con *l'oggetto e gli attributi dell'oggetto non possono essere modificati?
agente47

1
Solo una nota che questa raccomandazione sull'uso export functioninsieme exports.myfunc2sta tecnicamente mescolando la sintassi dei moduli commonjs e ES6 e questo non è consentito nelle versioni più recenti di webpack (2+) che richiedono l'utilizzo della sintassi del modulo ES6 tutto o niente. Di seguito ho aggiunto una risposta basata su questa che funzionerà in ambienti severi ES6.
QuarkleMotion

6

Ho implementato una libreria che tenta di risolvere il problema del derisione in fase di esecuzione delle importazioni della classe Typescript senza che la classe originale sia a conoscenza di alcuna iniezione di dipendenza esplicita.

La libreria utilizza la import * assintassi e quindi sostituisce l'oggetto esportato originale con una classe stub. Conserva la sicurezza del tipo in modo che i test si interrompano al momento della compilazione se un nome di metodo è stato aggiornato senza aggiornare il test corrispondente.

Questa libreria può essere trovata qui: ts-mock-imports .


1
Questo modulo ha bisogno di più stelle github
SD

6

La risposta di @ vdloo mi ha portato nella giusta direzione, ma l'uso sia delle parole chiave "export" del commonjs che del modulo ES6 "export" insieme nello stesso file non ha funzionato per me (webpack v2 o successive lamentele). Invece, sto usando un'esportazione predefinita (variabile denominata) che avvolge tutte le singole esportazioni del modulo denominato e quindi importa l'esportazione predefinita nel mio file di test. Sto usando la seguente configurazione di esportazione con mocha / sinon e lo stub funziona bene senza bisogno di ricablare, ecc .:

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

Risposta utile, grazie. Volevo solo ricordare che let MyModulenon è necessario utilizzare l'esportazione predefinita (può essere un oggetto non elaborato). Inoltre, questo metodo non richiede myfunc1()di chiamare myfunc2(), funziona solo per spiarlo direttamente.
Mark Edington,

@QuarkleMotion: sembra che tu l'abbia modificato con un account diverso dal tuo account principale per caso. Ecco perché la tua modifica ha dovuto passare attraverso un'approvazione manuale - non sembrava che fosse da te suppongo che si trattasse solo di un incidente, ma, se fosse intenzionale, dovresti leggere la politica ufficiale sui conti delle marionette per calzini così non violare accidentalmente le regole .
Conspicuous Compiler

1
@ConspicuousCompiler grazie per l'heads-up - questo è stato un errore, non avevo intenzione di modificare questa risposta con il mio account SO collegato via email.
QuarkleMotion

Questa sembra essere una risposta a una domanda diversa! Dov'è widget.js e network.js? Questa risposta sembra non avere alcuna dipendenza transitiva, che è ciò che ha reso difficile la domanda originale.
Bennett McElwee,

3

Ho trovato che questa sintassi funziona:

Il mio modulo:

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

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

Il codice di prova del mio modulo:

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

Vedere il doc .


+1 e con alcune istruzioni aggiuntive: sembra funzionare solo con i moduli del nodo, cioè cose che hai su package.json. E ancora più importante, qualcosa che non è menzionato nei documenti Jest, la stringa passata jest.mock()deve corrispondere al nome usato in import / packge.json invece che al nome di costante. Nei documenti sono entrambi uguali, ma con un codice come quello import jwt from 'jsonwebtoken'devi impostare il finto comejest.mock('jsonwebtoken')
kaskelotti,

0

Non l'ho provato da solo, ma penso che la beffa potrebbe funzionare. Ti permette di sostituire il modulo reale con un finto che hai fornito. Di seguito è riportato un esempio per darti un'idea di come funziona:

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

Sembra che mockerynon sia più gestito e penso che funzioni solo con Node.js, ma comunque è una soluzione pulita per i moduli beffardi che altrimenti sarebbero difficili da deridere.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.