¿Cómo puedo burlarme de las dependencias para las pruebas unitarias en RequireJS?

127

Tengo un módulo AMD que quiero probar, pero quiero burlarme de sus dependencias en lugar de cargar las dependencias reales. Estoy usando requirejs, y el código para mi módulo se parece a esto:

define(['hurp', 'durp'], function(Hurp, Durp) {
  return {
    foo: function () {
      console.log(Hurp.beans)
    },
    bar: function () {
      console.log(Durp.beans)
    }
  }
}

¿Cómo puedo burlar a cabo hurpy durppor lo que puede efectivamente prueba de unidad?

jergason
fuente
Solo estoy haciendo algunas cosas locas de evaluación en node.js para burlarme de la definefunción. Sin embargo, hay algunas opciones diferentes. Publicaré una respuesta con la esperanza de que sea útil.
jergason
1
Para las pruebas unitarias con Jasmine, es posible que también desee echar un vistazo rápido a Jasq . [Descargo de responsabilidad: estoy manteniendo la
biblioteca
1
Si está probando en el entorno de nodo, podría usar el paquete require-mock . Le permite burlarse fácilmente de sus dependencias, reemplazar módulos, etc. Si necesita env del navegador con la carga del módulo asíncrono, puede probar Squire.js
ValeriiVasin el

Respuestas:

64

Entonces, después de leer esta publicación, se me ocurrió una solución que utiliza la función de configuración requirejs para crear un nuevo contexto para su prueba donde simplemente puede burlarse de sus dependencias:

var cnt = 0;
function createContext(stubs) {
  cnt++;
  var map = {};

  var i18n = stubs.i18n;
  stubs.i18n = {
    load: sinon.spy(function(name, req, onLoad) {
      onLoad(i18n);
    })
  };

  _.each(stubs, function(value, key) {
    var stubName = 'stub' + key + cnt;

    map[key] = stubName;

    define(stubName, function() {
      return value;
    });
  });

  return require.config({
    context: "context_" + cnt,
    map: {
      "*": map
    },
    baseUrl: 'js/cfe/app/'
  });
}

Por lo tanto, crea un nuevo contexto donde las definiciones Hurpy Durpserán establecidas por los objetos que pasó a la función. El Math.random para el nombre está quizás un poco sucio pero funciona. Porque si tiene un montón de pruebas, necesita crear un nuevo contexto para cada suite para evitar reutilizar sus simulacros, o cargar simulacros cuando desee el módulo requirejs real.

En su caso, se vería así:

(function () {

  var stubs =  {
    hurp: 'hurp',
    durp: 'durp'
  };
  var context = createContext(stubs);

  context(['yourModuleName'], function (yourModule) {

    //your normal jasmine test starts here

    describe("yourModuleName", function () {
      it('should log', function(){
         spyOn(console, 'log');
         yourModule.foo();

         expect(console.log).toHasBeenCalledWith('hurp');
      })
    });
  });
})();

Así que estoy usando este enfoque en producción por un tiempo y es realmente robusto.

Andreas Köberle
fuente
1
Me gusta lo que estás haciendo aquí ... especialmente porque puedes cargar un contexto diferente para cada prueba. Lo único que desearía poder cambiar es que parece que solo funciona si me burlo de todas las dependencias. ¿Conoce una manera de devolver los objetos simulados si están allí, pero recurrir a la recuperación del archivo .js real si no se proporciona un simulacro? He estado tratando de investigar el código requerido para resolverlo, pero me estoy perdiendo un poco.
Glen Hughes
55
Solo se burla de la dependencia que pasa a la createContextfunción. Entonces, en su caso, si solo pasa {hurp: 'hurp'}a la función, el durparchivo se cargará como una dependencia normal.
Andreas Köberle
1
Estoy usando esto en Rails (con jasminerice / phantomjs) y ha sido la mejor solución que he encontrado para burlarme con RequireJS.
Ben Anderson
13
+1 No es bonito, pero de todas las posibles soluciones, esta parece ser la menos fea / desordenada. Este problema merece más atención.
Chris Salzberg
1
Actualización: a cualquiera que esté considerando esta solución, le sugiero que consulte squire.js ( github.com/iammerrick/Squire.js ) mencionado a continuación. Es una buena implementación de una solución similar a esta, que crea nuevos contextos donde sea que se necesiten apéndices.
Chris Salzberg
44

es posible que desee ver la nueva biblioteca Squire.js

de los documentos:

¡Squire.js es un inyector de dependencias para que los usuarios de Require.js faciliten burlarse de las dependencias!

busticado
fuente
2
¡Muy recomendado! Estoy actualizando mi código para usar squire.js y hasta ahora me gusta mucho. Código muy muy simple, sin gran magia bajo el capó, pero hecho de una manera que es (relativamente) fácil de entender.
Chris Salzberg
1
He tenido muchos problemas con el efecto secundario del escudero en otras pruebas y no puedo recomendarlo. Recomendaría npmjs.com/package/requirejs-mock
Jeff Whiting
17

He encontrado tres soluciones diferentes para este problema, ninguna de ellas agradable.

Definición de dependencias en línea

define('hurp', [], function () {
  return {
    beans: 'Beans'
  };
});

define('durp', [], function () {
  return {
    beans: 'durp beans'
  };
});

require('hurpdhurp', function () {
  // test hurpdurp in here
});

Fugly Tienes que abarrotar tus pruebas con muchas repeticiones de AMD.

Carga de dependencias simuladas de diferentes rutas

Esto implica el uso de un archivo config.js separado para definir rutas para cada una de las dependencias que apuntan a simulacros en lugar de las dependencias originales. Esto también es feo, ya que requiere la creación de toneladas de archivos de prueba y archivos de configuración.

Fingirlo en el nodo

Esta es mi solución actual, pero sigue siendo terrible.

Usted crea su propia definefunción para proporcionar sus propios simulacros al módulo y poner sus pruebas en la devolución de llamada. Luego, evalel módulo para ejecutar sus pruebas, así:

var fs = require('fs')
  , hurp = {
      beans: 'BEANS'
    }
  , durp = {
      beans: 'durp beans'
    }
  , hurpDurp = fs.readFileSync('path/to/hurpDurp', 'utf8');
  ;



function define(deps, cb) {
  var TestableHurpDurp = cb(hurp, durp);
  // now run tests below on TestableHurpDurp, which is using your
  // passed-in mocks as dependencies.
}

// evaluate the AMD module, running your mocked define function and your tests.
eval(hurpDurp);

Esta es mi solución preferida. Parece un poco mágico, pero tiene algunos beneficios.

  1. Ejecute sus pruebas en el nodo, para no meterse con la automatización del navegador.
  2. Menos necesidad de repetitivo AMD desordenado en sus pruebas.
  3. Puedes usarlo evalcon ira e imaginar a Crockford explotando de rabia.

Todavía tiene algunos inconvenientes, obviamente.

  1. Como está probando en el nodo, no puede hacer nada con los eventos del navegador o la manipulación del DOM. Solo es bueno para probar la lógica.
  2. Todavía un poco torpe para configurar. Debe simulacros defineen cada prueba, ya que allí es donde realmente se ejecutan sus pruebas.

Estoy trabajando en un corredor de prueba para dar una sintaxis más agradable para este tipo de cosas, pero todavía no tengo una buena solución para el problema 1.

Conclusión

Burlarse de los departamentos en requirejs es una mierda. Encontré una forma en que funciona, pero todavía no estoy muy contento con eso. Por favor, avíseme si tiene alguna idea mejor.

jergason
fuente
15

Hay una config.mapopción http://requirejs.org/docs/api.html#config-map .

Sobre cómo usarlo:

  1. Definir módulo normal;
  2. Definir módulo de código auxiliar;
  3. Configure RequireJS explícitamente;

    requirejs.config({
      map: {
        'source/js': {
          'foo': 'normalModule'
        },
        'source/test': {
          'foo': 'stubModule'
        }
      }
    });

En este caso, para el código normal y de prueba, puede usar el foomódulo, que será una referencia real del módulo y el código auxiliar correspondiente.

Artem Oboturov
fuente
Este enfoque funcionó muy bien para mí. En mi caso, agregué esto al html de la página del corredor de prueba -> map: {'*': {'Common / Modules / utilityModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Aligned
9

Puede usar testr.js para burlarse de las dependencias. Puede configurar testr para cargar las dependencias simuladas en lugar de las originales. Aquí hay un ejemplo de uso:

var fakeDep = function(){
    this.getText = function(){
        return 'Fake Dependancy';
    };
};

var Module1 = testr('module1', {
    'dependancies/dependancy1':fakeDep
});

Mira esto también: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

janith
fuente
2
Tenía muchas ganas de que testr.js funcionara, pero todavía no se siente a la altura. Al final, voy con la solución de @Andreas Köberle, que agregará contextos anidados a mis pruebas (no bonitos) pero que funciona constantemente. Desearía que alguien pudiera concentrarse en resolver esta solución de una manera más elegante. Seguiré mirando testr.js y, si funciona, haré el cambio.
Chris Salzberg
@shioyama hola, gracias por los comentarios! Me encantaría ver cómo ha configurado testr.js dentro de su pila de pruebas. ¡Feliz de ayudarlo a solucionar cualquier problema que pueda tener! También está la página de problemas de github si desea registrar algo allí. Gracias,
Matty F
1
@MattyF lo siento, ni siquiera recuerdo en este momento cuál fue la razón exacta por la que testr.js no funcionó para mí, pero he llegado a la conclusión de que el uso de contextos adicionales está bastante bien y de hecho está en línea con cómo require.js estaba destinado a ser utilizado para burlarse / tropezar.
Chris Salzberg
2

Esta respuesta se basa en la respuesta de Andreas Köberle .
No fue tan fácil para mí implementar y comprender su solución, por lo que lo explicaré con un poco más de detalle cómo funciona, y algunas dificultades para evitar, con la esperanza de que ayude a futuros visitantes.

Entonces, primero que nada la configuración:
estoy usando Karma como corredor de prueba y MochaJs como marco de prueba.

Usar algo como Squire no funcionó para mí, por alguna razón, cuando lo usé, el marco de prueba arrojó errores:

TypeError: no se puede leer la propiedad 'call' de undefined

RequireJs tiene la posibilidad de asignar identificadores de módulo a otros identificadores de módulo. También permite crear una requirefunción que utiliza una configuración diferente a la global require.
Estas características son cruciales para que esta solución funcione.

Aquí está mi versión del código simulado, que incluye (muchos) comentarios (espero que sea comprensible). Lo envolví dentro de un módulo, para que las pruebas puedan requerirlo fácilmente.

define([], function () {
    var count = 0;
    var requireJsMock= Object.create(null);
    requireJsMock.createMockRequire = function (mocks) {
        //mocks is an object with the module ids/paths as keys, and the module as value
        count++;
        var map = {};

        //register the mocks with unique names, and create a mapping from the mocked module id to the mock module id
        //this will cause RequireJs to load the mock module instead of the real one
        for (property in mocks) {
            if (mocks.hasOwnProperty(property)) {
                var moduleId = property;  //the object property is the module id
                var module = mocks[property];   //the value is the mock
                var stubId = 'stub' + moduleId + count;   //create a unique name to register the module

                map[moduleId] = stubId;   //add to the mapping

                //register the mock with the unique id, so that RequireJs can actually call it
                define(stubId, function () {
                    return module;
                });
            }
        }

        var defaultContext = requirejs.s.contexts._.config;
        var requireMockContext = { baseUrl: defaultContext.baseUrl };   //use the baseUrl of the global RequireJs config, so that it doesn't have to be repeated here
        requireMockContext.context = "context_" + count;    //use a unique context name, so that the configs dont overlap
        //use the mapping for all modules
        requireMockContext.map = {
            "*": map
        };
        return require.config(requireMockContext);  //create a require function that uses the new config
    };

    return requireJsMock;
});

El mayor obstáculo que encontré, que literalmente me costó horas, fue crear la configuración RequireJs. Traté de copiarlo (en profundidad) y solo anular las propiedades necesarias (como contexto o mapa). ¡Esto no funciona! Solo copie el baseUrl, esto funciona bien.

Uso

Para usarlo, solicítelo en su prueba, cree los simulacros y luego páselo createMockRequire. Por ejemplo:

var ModuleMock = function () {
    this.method = function () {
        methodCalled += 1;
    };
};
var mocks = {
    "ModuleIdOrPath": ModuleMock
}
var requireMocks = mocker.createMockRequire(mocks);

Y aquí un ejemplo de un archivo de prueba completo :

define(["chai", "requireJsMock"], function (chai, requireJsMock) {
    var expect = chai.expect;

    describe("Module", function () {
        describe("Method", function () {
            it("should work", function () {
                return new Promise(function (resolve, reject) {
                    var handler = { handle: function () { } };

                    var called = 0;
                    var moduleBMock = function () {
                        this.method = function () {
                            methodCalled += 1;
                        };
                    };
                    var mocks = {
                        "ModuleBIdOrPath": moduleBMock
                    }
                    var requireMocks = requireJsMock.createMockRequire(mocks);

                    requireMocks(["js/ModuleA"], function (moduleA) {
                        try {
                            moduleA.method();   //moduleA should call method of moduleBMock
                            expect(called).to.equal(1);
                            resolve();
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            });
        });
    });
});
Domysee
fuente
0

si desea hacer algunas pruebas js simples que aíslan una unidad, simplemente puede usar este fragmento:

function define(args, func){
    if(!args.length){
        throw new Error("please stick to the require.js api which wants a: define(['mydependency'], function(){})");
    }

    var fileName = document.scripts[document.scripts.length-1].src;

    // get rid of the url and path elements
    fileName = fileName.split("/");
    fileName = fileName[fileName.length-1];

    // get rid of the file ending
    fileName = fileName.split(".");
    fileName = fileName[0];

    window[fileName] = func;
    return func;
}
window.define = define;
usuario3033599
fuente