¿Cómo burlarse de las importaciones de un módulo ES6?

141

Tengo los siguientes módulos 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() {
    ...
  }
}

Estoy buscando una forma de probar Widget con una instancia simulada de getDataFromServer. Si usara <script>s separados en lugar de módulos ES6, como en Karma, podría escribir mi prueba como:

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

Sin embargo, si estoy probando módulos ES6 individualmente fuera de un navegador (como con Mocha + babel), escribiría algo como:

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

Está bien, pero ahora getDataFromServerno está disponible en window(bueno, no hay ninguno window), y no sé cómo inyectar cosas directamente en widget.jsel ámbito de aplicación.

Entonces, ¿a dónde voy desde aquí?

  1. ¿Hay alguna manera de acceder al alcance de widget.js, o al menos reemplazar sus importaciones con mi propio código?
  2. Si no, ¿cómo puedo hacer Widgetcomprobable?

Cosas que consideré:

a. Inyección manual de dependencias.

Elimine todas las importaciones widget.jsy espere que la persona que llama proporcione los departamentos.

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

Me incomoda mucho estropear la interfaz pública de Widget como esta y exponer los detalles de implementación. No vayas.


si. Exponer las importaciones para permitir burlarse de ellos.

Algo como:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

luego:

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

Esto es menos invasivo, pero requiere que escriba mucho repetitivo para cada módulo, y todavía existe el riesgo de que lo use en getDataFromServerlugar de deps.getDataFromServertodo el tiempo. Estoy inquieto al respecto, pero esa es mi mejor idea hasta ahora.

Kos
fuente
Si no hay soporte simulado nativo para este tipo de importación, probablemente pensaría en escribir un transformador propio para que Babel convierta su importación de estilo ES6 en un sistema de importación simulable personalizado. Esto seguramente agregaría otra capa de posible falla y cambiará el código que desea probar, ....
t.niese
No puedo configurar una suite de prueba en este momento, pero trataría de usar la función de jasmincreateSpy ( github.com/jasmine/jasmine/blob/… ) con una referencia importada a getDataFromServer del módulo 'network.js'. De modo que, en el archivo de pruebas del widget, importaría getDataFromServer y luego lo haríalet spy = createSpy('getDataFromServer', getDataFromServer)
Microfed
La segunda suposición es devolver un objeto del módulo 'network.js', no una función. De esa manera, podría spyOnen ese objeto, importado denetwork.js módulo. Siempre es una referencia al mismo objeto.
Microfed
En realidad, ya es un objeto, por lo que puedo ver: babeljs.io/repl/…
Microfed
2
¿Realmente no entiendo cómo la inyección de dependencia desordena Widgetla interfaz pública? Widgetestá en mal estado sin deps . ¿Por qué no hacer explícita la dependencia?
thebearingedge

Respuestas:

129

Comencé a emplear el import * as objestilo dentro de mis pruebas, que importa todas las exportaciones de un módulo como propiedades de un objeto que luego se puede burlar. Creo que esto es mucho más limpio que usar algo como recablear o proxyquire o cualquier técnica similar. Lo he hecho con mayor frecuencia cuando necesito burlarme de las acciones de Redux, por ejemplo. Esto es lo que podría usar para su ejemplo anterior:

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 su función resulta ser una exportación predeterminada, entonces import * as network from './network' produciría {default: getDataFromServer}y puede burlarse de network.default.

carpeliam
fuente
3
¿Utiliza el import * as objúnico en la prueba o también en su código regular?
Chau Thai
36
@carpeliam Esto no funcionará con la especificación del módulo ES6 donde las importaciones son de solo lectura.
ashish
77
Jasmine se queja, lo [method_name] is not declared writable or has no setterque tiene sentido ya que las importaciones de es6 son constantes. ¿Hay alguna forma de solución?
lpan
2
@Francisc import(a diferencia de require, que puede ir a cualquier parte) se levanta, por lo que técnicamente no puede importar varias veces. ¿Suena como si tu espía fuera llamado a otra parte? Para evitar que las pruebas se confundan (conocido como contaminación de prueba), puede restablecer sus espías en un AfterEach (por ejemplo, sinon.sandbox). Jasmine creo que hace esto automáticamente.
carpeliam
10
@ agent47 El problema es que, si bien la especificación ES6 impide específicamente que esta respuesta funcione, exactamente de la manera que usted mencionó, la mayoría de las personas que escriben importen su JS realmente no usan módulos ES6. Algo como webpack o babel intervendrá en el momento de la construcción y lo convertirá en su propio mecanismo interno para llamar a partes distantes del código (por ejemplo __webpack_require__) o en uno de los estándares de facto anteriores a ES6 , CommonJS, AMD o UMD. Y esa conversión a menudo no se adhiere estrictamente a las especificaciones. Entonces, para muchos, muchos desarrolladores en este momento, esta respuesta funciona bien. Por ahora.
daemonexmachina
31

@carpeliam es correcto, pero tenga en cuenta que si desea espiar una función en un módulo y usar otra función en ese módulo que llama a esa función, debe llamar a esa función como parte del espacio de nombres de exportaciones; de lo contrario, el espía no se usará.

Ejemplo equivocado:

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

Ejemplo correcto:

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
fuente
44
¡Ojalá pudiera votar esta respuesta 20 veces más! ¡Gracias!
sfletche
¿Alguien puede explicar por qué este es el caso? ¿Export.myfunc2 () es una copia de myfunc2 () sin ser una referencia directa?
Colin Whitmarsh
2
@ColinWhitmarsh exports.myfunc2es una referencia directa myfunc2hasta que lo spyOnreemplaza con una referencia a una función de espía. spyOncambiará el valor de exports.myfunc2y lo reemplazará con un objeto espía, mientras que myfunc2permanece intacto en el alcance del módulo (porque spyOnno tiene acceso a él)
madprog
no debería importar con *congelar el objeto y los atributos del objeto no se pueden cambiar?
agent47
1
Solo una nota de que esta recomendación de usar export functionjunto con exports.myfunc2está técnicamente mezclando la sintaxis del módulo commonjs y ES6 y esto no está permitido en las versiones más recientes de webpack (2+) que requieren el uso de la sintaxis del módulo ES6 todo o nada. Agregué una respuesta a continuación basada en esta que funcionará en entornos estrictos de ES6.
QuarkleMotion
6

Implementé una biblioteca que intenta resolver el problema de la burla en tiempo de ejecución de las importaciones de la clase Typecript sin necesidad de que la clase original conozca ninguna inyección explícita de dependencia.

La biblioteca usa la import * assintaxis y luego reemplaza el objeto exportado original con una clase stub. Conserva la seguridad de tipo, por lo que sus pruebas se interrumpirán en el momento de la compilación si se actualizó el nombre de un método sin actualizar la prueba correspondiente.

Esta biblioteca se puede encontrar aquí: ts-mock-imports .

EmandM
fuente
1
Este módulo necesita más estrellas github
SD
6

La respuesta de @ vdloo me llevó en la dirección correcta, pero usar las palabras clave "exportar" del módulo ES6 "exportar" en el mismo archivo no funcionó para mí (webpack v2 o posterior se queja). En cambio, estoy usando una exportación predeterminada (variable con nombre) que envuelve todas las exportaciones individuales de módulos con nombre y luego importo la exportación predeterminada en mi archivo de pruebas. Estoy usando la siguiente configuración de exportación con mocha / sinon y el stubbing funciona bien sin necesidad de volver a cablear, 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);
  });
});
QuarkleMotion
fuente
Respuesta útil, gracias. Solo quería mencionar que let MyModuleno es necesario usar la exportación predeterminada (puede ser un objeto sin formato). Además, este método no requiere myfunc1()llamar myfunc2(), solo funciona para espiarlo directamente.
Mark Edington
@QuarkleMotion: Parece que lo editó con una cuenta diferente a su cuenta principal por accidente. Es por eso que su edición tuvo que pasar por una aprobación manual: no parecía que fuera de usted , supongo que esto fue solo un accidente, pero, si fue intencional, debería leer la política oficial sobre cuentas de títeres de calcetines para que pueda no viole accidentalmente las reglas .
Compilador conspicuo
1
@ConspicuousCompiler, gracias por el aviso: esto fue un error, no tenía la intención de modificar esta respuesta con mi cuenta SO vinculada al correo electrónico del trabajo.
QuarkleMotion
¡Esto parece ser una respuesta a una pregunta diferente! ¿Dónde están widget.js y network.js? Esta respuesta parece no tener dependencia transitiva, que es lo que hizo difícil la pregunta original.
Bennett McElwee
3

He encontrado que esta sintaxis funciona:

Mi modulo:

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

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

Código de prueba de mi módulo:

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

Ver el doc .

nerfólogo
fuente
+1 y con algunas instrucciones adicionales: Parece que solo funciona con módulos de nodo, es decir, cosas que tiene en package.json. Y lo más importante es que, algo que no se menciona en los documentos de Jest, la cadena que se pasa jest.mock()tiene que coincidir con el nombre utilizado en import / packge.json en lugar del nombre de constante. En los documentos, ambos son iguales, pero con un código como el import jwt from 'jsonwebtoken'que necesita configurar el simulacro comojest.mock('jsonwebtoken')
kaskelotti
0

No lo he intentado yo mismo, pero creo que la burla podría funcionar. Le permite sustituir el módulo real con un simulacro que haya proporcionado. A continuación se muestra un ejemplo para darle una idea de cómo funciona:

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

Parece que mockeryya no se mantiene y creo que solo funciona con Node.js, pero no obstante, es una buena solución para burlarse de módulos que de otro modo serían difíciles de burlar.

Erik B
fuente