web-dev-qa-db-fra.com

Comment se moquer des importations d'un module ES6?

J'ai les modules ES6 suivants:

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

Je cherche un moyen de tester Widget avec une instance fictive de getDataFromServer. Si j'utilisais <script>s séparé au lieu de modules ES6, comme dans Karma, je pourrais écrire mon test comme suit:

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

Cependant, si je teste les modules ES6 individuellement en dehors d'un navigateur (comme avec Mocha + babel), j'écrirais quelque chose comme:

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, mais maintenant, getDataFromServer n'est pas disponible dans window (enfin, il n'y a pas du tout de window), et je ne connais pas le moyen d'injecter directement des éléments dans widget.js. propre portée.

Alors, où vais-je d'ici?

  1. Y a-t-il un moyen d'accéder à la portée de widget.js, ou du moins de remplacer ses importations par mon propre code?
  2. Si non, comment puis-je rendre Widget testable?

Des choses que j'ai envisagées:

une. Injection de dépendance manuelle.

Supprimez toutes les importations de widget.js et attendez que l'appelant fournisse les dépôts.

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

Je suis très mal à l'aise de gâcher l'interface publique de Widget comme celle-ci et d'exposer les détails de la mise en œuvre. Ne pas aller.


b. Exposer les importations pour permettre de se moquer d'eux.

Quelque chose comme:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

ensuite:

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

Ceci est moins invasif mais nécessite que je rédige beaucoup de passe-partout pour chaque module, et il y a toujours un risque pour moi d'utiliser getDataFromServer au lieu de deps.getDataFromServer tout le temps. Je suis inquiet à ce sujet, mais c'est ma meilleure idée jusqu'à présent.

128
Kos

J'ai commencé à utiliser le style import * as obj dans mes tests, qui importe toutes les exportations d'un module en tant que propriétés d'un objet qui peut ensuite être simulé. Je trouve que cela est beaucoup plus propre que d'utiliser quelque chose comme rewire ou proxyquire ou toute technique similaire. Je l'ai fait le plus souvent lorsque j'ai besoin de me moquer des actions Redux, par exemple. Voici ce que je pourrais utiliser pour votre exemple ci-dessus:

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

Si votre fonction est une exportation par défaut, alors import * as network from './network' produira {default: getDataFromServer} et vous pourrez vous moquer de network.default.

117
carpeliam

@carpeliam est correct, mais notez que si vous souhaitez espionner une fonction dans un module et utiliser une autre fonction de ce module appelant cette fonction, vous devez appeler cette fonction dans le cadre de l'espace de noms des exportations, sinon l'espion ne sera pas utilisé.

Mauvais exemple:

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

Bon exemple:

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

La réponse de @vdloo m'a fait aller dans la bonne direction, mais utiliser les mots clés "export" de commonjs et le mot "export" du module ES6 dans le même fichier ne fonctionnait pas pour moi (Webpack v2 ou version ultérieure se plaint). Au lieu de cela, j'utilise une exportation par défaut (variable nommée) englobant toutes les exportations individuelles de modules nommés, puis important l'exportation par défaut dans mon fichier de test. J'utilise la configuration d'exportation suivante avec mocha/sinon et le stubbing fonctionne bien sans avoir besoin de recâbler, etc.:

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

J'ai implémenté une bibliothèque qui tente de résoudre le problème du moquage au moment de l'exécution des importations de classe TypeScript sans qu'il soit nécessaire que la classe d'origine connaisse une injection de dépendance explicite.

La bibliothèque utilise la syntaxe import * as puis remplace l'objet exporté d'origine par une classe de raccord. Il conserve la sécurité de type afin que vos tests soient interrompus au moment de la compilation si un nom de méthode a été mis à jour sans mettre à jour le test correspondant.

Cette bibliothèque peut être trouvée ici: ts-mock-imports .

4
EmandM

J'ai trouvé cette syntaxe fonctionner:

Mon module:

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

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

Le code de test de mon module:

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

Voir la doc .

3
nerfologist