¿Cómo probar un módulo Node.js que requiere otros módulos y cómo burlarse de la función global requerida?


156

Este es un ejemplo trivial que ilustra el quid de mi problema:

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

Estoy tratando de escribir una prueba unitaria para este código. ¿Cómo puedo burlarme del requisito para el innerLibsin burlar completamente la requirefunción?

Así que este soy yo tratando de burlarme de lo global requirey descubriendo que no funcionará incluso para hacer eso:

var path = require('path'),
    vm = require('vm'),
    fs = require('fs'),
    indexPath = path.join(__dirname, './underTest');

var globalRequire = require;

require = function(name) {
    console.log('require: ' + name);
    switch(name) {
        case 'connect':
        case indexPath:
            return globalRequire(name);
            break;
    }
};

El problema es que la requirefunción dentro del underTest.jsarchivo en realidad no se ha burlado. Todavía apunta a la requirefunción global . Por lo tanto, parece que solo puedo requiresimular la función dentro del mismo archivo en el que estoy haciendo el simulacro . Si uso el global requirepara incluir algo, incluso después de haber anulado la copia local, los archivos requeridos seguirán teniendo requirereferencia global


Tienes que sobrescribir global.require. Las variables escriben modulede forma predeterminada ya que los módulos tienen un alcance de módulo.
Raynos

@Raynos ¿Cómo haría eso? global.require es indefinido? Incluso si lo reemplazo con mi propia función, ¿otras funciones nunca lo usarían?
HMR

Respuestas:


175

¡Tu puedes ahora!

Publiqué proxyquire que se encargará de anular el mundial requieren dentro de su módulo mientras se está probando la misma.

Esto significa que no necesita cambios en su código para inyectar simulacros para los módulos requeridos.

Proxyquire tiene una API muy simple que permite resolver el módulo que está tratando de probar y pasar simulaciones / apéndices para sus módulos requeridos en un solo paso simple.

@Raynos tiene razón en que tradicionalmente tenía que recurrir a soluciones no muy ideales para lograr eso o hacer un desarrollo ascendente

Cuál es la razón principal por la que creé proxyquire: para permitir el desarrollo de prueba de arriba hacia abajo sin ninguna molestia.

Eche un vistazo a la documentación y los ejemplos para determinar si se ajusta a sus necesidades.


55
Uso proxyquire y no puedo decir suficientes cosas buenas. Me salvó! Me encargaron escribir pruebas de nodos de jazmín para una aplicación desarrollada en el acelerador Titanium que obliga a algunos módulos a ser rutas absolutas y muchas dependencias circulares. proxyquire me permitió detener esos espacios y burlarme de la basura que no necesitaba para cada prueba. (Explicado aquí ). ¡Muchas gracias!
Sukima

Me alegra saber que proxyquire te ayudó a probar tu código correctamente :)
Thorsten Lorenz

1
muy agradable @ThorstenLorenz, lo defiendo. estar usando proxyquire!
bevacqua

¡Fantástico! Cuando vi la respuesta aceptada de que "no puedes" pensé "¡Oh Dios, en serio?" pero esto realmente lo salvó.
Chadwick

3
Para aquellos de ustedes que usan Webpack, no pasen tiempo investigando proxyquire. No es compatible con Webpack. Estoy buscando inject-loader en su lugar ( github.com/plasticine/inject-loader ).
Artif3x

116

Una mejor opción en este caso es burlarse de los métodos del módulo que se devuelve.

Para bien o para mal, la mayoría de los módulos node.js son singletons; dos piezas de código que requieren () el mismo módulo obtienen la misma referencia a ese módulo.

Puede aprovechar esto y usar algo como sinon para burlarse de los elementos que se requieren. prueba de moca sigue:

// in your testfile
var innerLib  = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon     = require('sinon');

describe("underTest", function() {
  it("does something", function() {
    sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
      // whatever you would like innerLib.toCrazyCrap to do under test
    });

    underTest();

    sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion

    innerLib.toCrazyCrap.restore(); // restore original functionality
  });
});

Sinon tiene una buena integración con chai para hacer afirmaciones, y escribí un módulo para integrar sinon con mocha para permitir una limpieza más fácil de espías / trozos (para evitar la contaminación de prueba).

Tenga en cuenta que underTest no se puede burlar de la misma manera, ya que underTest solo devuelve una función.

Otra opción es usar simulacros de Jest. Seguimiento en su página


1
Desafortunadamente, no se garantiza que los módulos node.js sean singletons, como se explica aquí: justjs.com/posts/…
FrontierPsycho

44
@FrontierPsycho algunas cosas: Primero, en lo que respecta a las pruebas, el artículo es irrelevante. Mientras esté probando sus dependencias (y no dependencias de dependencias), todo su código obtendrá el mismo objeto cuando usted require('some_module'), porque todo su código comparte el mismo directorio node_modules. En segundo lugar, el artículo combina espacio de nombres con singletons, que es algo ortogonal. En tercer lugar, ese artículo es bastante antiguo (en lo que respecta a node.js), por lo que lo que podría haber sido válido en ese momento posiblemente no lo sea ahora.
Elliot Foster

2
Hm. A menos que uno de nosotros desenterre un código que pruebe un punto u otro, elegiría su solución de inyección de dependencia, o simplemente pasar objetos, es más seguro y más a prueba de futuro.
FrontierPsycho

1
No estoy seguro de lo que estás pidiendo que se pruebe. La naturaleza singleton (en caché) de los módulos de nodo se entiende comúnmente. La inyección de dependencia, si bien es una buena ruta, puede ser mucho más placa de caldera y más código. DI es más común en lenguajes estáticamente tipados, donde es más difícil eludir espías / stubs / simulacros en su código dinámicamente. Varios proyectos que he realizado en los últimos tres años utilizan el método descrito en mi respuesta anterior. Es el más fácil de todos los métodos, aunque lo uso con moderación.
Elliot Foster

1
Te sugiero que leas en sinon.js. Si está utilizando sinon (como en el ejemplo anterior) que lo haría bien innerLib.toCrazyCrap.restore()y restub, o llamar sinon a través de sinon.stub(innerLib, 'toCrazyCrap')la cual le permite cambiar cómo se comporta el talón: innerLib.toCrazyCrap.returns(false). Además, rewire parece ser muy similar a la proxyquireextensión anterior.
Elliot Foster

11

Yo uso simulacro de requerimiento . Asegúrese de definir sus simulacros antes requiredel módulo que se probará.


También es bueno hacer stop (<file>) o stopAll () de inmediato para que no obtenga un archivo en caché en una prueba en la que no desea el simulacro.
Justin Kruse

1
Esto ayudó mucho.
wallop

2

Burlarse me requireparece un truco desagradable. Yo personalmente trataría de evitarlo y refactorizar el código para hacerlo más comprobable. Hay varios enfoques para manejar las dependencias.

1) pasar dependencias como argumentos

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

Esto hará que el código sea universalmente comprobable. La desventaja es que necesita pasar dependencias, lo que puede hacer que el código se vea más complicado.

2) implementar el módulo como una clase, luego usar métodos / propiedades de clase para obtener dependencias

(Este es un ejemplo artificial, donde el uso de la clase no es razonable, pero transmite la idea) (ejemplo ES6)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

Ahora puede tropezar fácilmente el getInnerLibmétodo para probar su código. El código se vuelve más detallado, pero también más fácil de probar.


1
No creo que sea hacky como supones ... esta es la esencia de la burla. Burlarse de las dependencias requeridas hace las cosas tan simples que le da control al desarrollador sin cambiar la estructura del código. Sus métodos son demasiado detallados y, por lo tanto, difíciles de razonar. Elijo proxyrequire o simulacro de requerimiento sobre esto; No veo ningún problema aquí. El código es limpio y fácil de razonar y recordar, la mayoría de las personas que leen esto ya tienen un código escrito que usted quiere que compliquen. Si estas bibliotecas son hack, entonces, según su definición, las burlas y los trozos también lo son y deberían detenerse.
Emmanuel Mahuni

1
El problema con el enfoque n. ° 1 es que está pasando detalles de implementación internos a la pila. Con múltiples capas, se vuelve mucho más complicado ser un consumidor de su módulo. Sin embargo, puede funcionar con un enfoque similar a un contenedor IOC para que las dependencias se inyecten automáticamente, sin embargo, parece que ya que tenemos dependencias inyectadas en los módulos de nodo a través de la declaración de importaciones, entonces tiene sentido poder burlarse de ellas en ese nivel .
magritte

1) Esto simplemente mueve el problema a otro archivo 2) todavía carga el otro módulo y, por lo tanto, impone una sobrecarga de rendimiento y posiblemente causa efectos secundarios (como el colorsmódulo popular que se mete String.prototype)
ThomasR

2

Si alguna vez has usado jest, entonces probablemente estés familiarizado con la función simulada de jest.

Usando "jest.mock (...)" simplemente puede especificar la cadena que ocurriría en una declaración require en su código en algún lugar y cada vez que se requiera un módulo usando esa cadena se devolvería un objeto simulado.

Por ejemplo

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

reemplazaría por completo todas las importaciones / requisitos de "firebase-admin" con el objeto que devolvió de esa función "fábrica".

Bueno, puede hacer eso cuando usa jest porque jest crea un tiempo de ejecución alrededor de cada módulo que ejecuta e inyecta una versión "enganchada" de require en el módulo, pero no podría hacerlo sin jest.

He intentado lograr esto con simulacro de requerimiento, pero para mí no funcionó para niveles anidados en mi fuente. Eche un vistazo al siguiente problema en github: mock-require no siempre se llama con Mocha .

Para abordar esto, he creado dos módulos npm que puede usar para lograr lo que desea.

Necesita un plugin de babel y un burlador de módulos.

En su .babelrc use el complemento babel-plugin-mock-require con las siguientes opciones:

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

y en su archivo de prueba use el módulo jestlike-mock de esta manera:

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

El jestlike-mockmódulo sigue siendo muy rudimentario y no tiene mucha documentación, pero tampoco hay mucho código. Agradezco cualquier RP para un conjunto de características más completo. El objetivo sería recrear toda la función "jest.mock".

Para ver cómo implementa jest, se puede buscar el código en el paquete "jest-runtime". Ver https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 por ejemplo, aquí generan un "bloqueo automático" de un módulo.

Espero que ayude ;)


1

No puedes Debe construir su conjunto de pruebas unitarias para que los módulos más bajos se prueben primero y los módulos de nivel superior que requieren módulos se prueben después.

También debe suponer que cualquier código de terceros y node.js en sí está bien probado.

Supongo que en un futuro próximo verán marcos burlones que sobrescribirán global.require

Si realmente debe inyectar un simulacro, puede cambiar su código para exponer el alcance modular.

// underTest.js
var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.toCrazyCrap();
}

module.exports = underTest;
module.exports.__module = module;

// test.js
function test() {
    var underTest = require("underTest");
    underTest.__module.innerLib = {
        toCrazyCrap: function() { return true; }
    };
    assert.ok(underTest());
}

Tenga en cuenta que esto se expone .__moduleen su API y cualquier código puede acceder al alcance modular bajo su propio riesgo.


2
Asumir que el código de un tercero está bien probado no es una excelente manera de trabajar con IMO.
henry.oswald

55
@beck es una excelente manera de trabajar. Te obliga a trabajar solo con código de terceros de alta calidad o escribir todas las partes de tu código para que cada dependencia esté bien probada
Raynos

Ok, pensé que te referías a no hacer pruebas de integración entre tu código y el código de terceros. Convenido.
henry.oswald

1
Un "conjunto de pruebas unitarias" es solo una colección de pruebas unitarias, pero las pruebas unitarias deben ser independientes entre sí, por lo tanto, la unidad en la prueba unitaria. Para ser utilizable, las pruebas unitarias deben ser rápidas e independientes, de modo que pueda ver claramente dónde se rompe el código cuando falla una prueba unitaria.
Andreas Berheim Brudin

Esto no funcionó para mí. El objeto del módulo no expone el "var innerLib ...", etc.
AnitKryst

1

Puedes usar la biblioteca de burlas :

describe 'UnderTest', ->
  before ->
    mockery.enable( warnOnUnregistered: false )
    mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
    @underTest = require('./path/to/underTest')

  it 'should compute complex value', ->
    expect(@underTest()).to.eq 'Complex result'

1

Código simple para burlarse de módulos para curiosos

Observe las partes donde manipula el método require.cachey nota require.resolveya que esta es la salsa secreta.

class MockModules {  
  constructor() {
    this._resolvedPaths = {} 
  }
  add({ path, mock }) {
    const resolvedPath = require.resolve(path)
    this._resolvedPaths[resolvedPath] = true
    require.cache[resolvedPath] = {
      id: resolvedPath,
      file: resolvedPath,
      loaded: true,
      exports: mock
    }
  }
  clear(path) {
    const resolvedPath = require.resolve(path)
    delete this._resolvedPaths[resolvedPath]
    delete require.cache[resolvedPath]
  }
  clearAll() {
    Object.keys(this._resolvedPaths).forEach(resolvedPath =>
      delete require.cache[resolvedPath]
    )
    this._resolvedPaths = {}
  }
}

Usar como :

describe('#someModuleUsingTheThing', () => {
  const mockModules = new MockModules()
  beforeAll(() => {
    mockModules.add({
      // use the same require path as you normally would
      path: '../theThing',
      // mock return an object with "theThingMethod"
      mock: {
        theThingMethod: () => true
      }
    })
  })
  afterAll(() => {
    mockModules.clearAll()
  })
  it('should do the thing', async () => {
    const someModuleUsingTheThing = require('./someModuleUsingTheThing')
    expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
  })
})

PERO ... proxyquire es bastante impresionante y deberías usar eso. Mantiene sus anulaciones requeridas localizadas solo para pruebas y lo recomiendo encarecidamente.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.