2012-07-11 12 views
126

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 de mi módulo es como la siguiente:¿Cómo puedo simular dependencias para pruebas unitarias en RequireJS?

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 hurp y durp por lo que puede efectivamente prueba de unidad?

+0

sólo estoy haciendo algunas cosas eval loco en Node.js para burlarse de la función 'define'. Sin embargo, hay algunas opciones diferentes. Publicaré una respuesta con la esperanza de que sea útil. – jergason

+1

Para probar la unidad con Jasmine es posible que también desee echar un vistazo rápido a [Jasq] (https://github.com/biril/jasq). [Descargo de responsabilidad: Estoy manteniendo la lib] – biril

+1

Si está probando en nodo env puede usar el paquete [require-mock] (https://github.com/ValeriiVasin/requirejs-mock). Le permite burlarse fácilmente de sus dependencias, reemplazar módulos, etc. Si necesita un navegador env con carga de módulo asíncrono, puede probar [Squire.js] (https://github.com/iammerrick/Squire.js/) – ValeriiVasin

Respuesta

16

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

Definición de dependencias Inline

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

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

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

Fugly. Tienes que llenar tus pruebas con muchos anuncios repetitivos de AMD.

Cargando Mock Dependencias de diferentes rutas

Esto implica el uso de un archivo config.js separada para definir rutas para cada una de las dependencias que a punto se burla en lugar de las dependencias originales. Esto también es feo y requiere la creación de toneladas de archivos de prueba y archivos de configuraciones.

Falsifiqúelo En Nodo

Ésta es mi solución actual, pero sigue siendo uno terrible.

Crea su propia función define para proporcionar sus propios simulacros al módulo y poner sus pruebas en la devolución de llamada. A continuación, el módulo de eval a ejecutar las 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); 

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

  1. Ejecute sus pruebas en el nodo, por lo que no hay problemas con la automatización del navegador.
  2. Menos necesidad de repetición AMD en sus pruebas.
  3. Llegas a usar eval con ira, e imaginas a Crockford explotando de rabia.

Todavía tiene algunos inconvenientes, obviamente.

  1. Dado que está realizando pruebas en un 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. Necesita simular define en todas las pruebas, ya que es donde realmente se ejecutan las 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

Burlándose deps en RequireJS chupa duro . Encontré una manera en la que funciona, pero aún no estoy muy contento con ella. Por favor, hágame saber si tiene alguna idea mejor.

64

Así que después de leer this post me ocurrió una solución que utilice la función de RequireJS de configuración para crear un nuevo contexto para la prueba en la que sólo 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 que crea un nuevo contexto en el las definiciones para Hurp y Durp se establecerán por los objetos que pasó a la función. El Math.random para el nombre es quizás un poco sucio pero funciona. Porque si tienes un montón de pruebas necesitas crear un nuevo contexto para cada suite para evitar reutilizar tus simulacros, o para cargar simulaciones cuando quieras 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 la producción por un tiempo y es muy robusto.

+1

Me gusta qué 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. ¿Conoces una forma de devolver los objetos de simulación si están allí, pero recurrir a la recuperación del archivo .js real si no se proporciona una simulación? He estado tratando de encontrar el código requerido para resolverlo, pero me estoy perdiendo un poco. –

+5

Solo se burla de la dependencia que pasas a la función 'createContext'. Entonces en tu caso, si solo pasas '{hurp: 'hurp'}' para funcionar, el archivo 'durp' se cargará como una dependencia normal. –

+0

Tienes razón ... no estoy seguro de lo que estaba haciendo mal la primera vez. Buen trabajo, esto va a ser útil ... ¡Muchas gracias! –

15

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

Sobre cómo-a utilizarlo:

  1. Definir módulo normal;
  2. Definir módulo de stub;
  3. Configure RequireJS expicitamente;

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

En este caso, para el código normal y la prueba se puede utilizar el módulo de foo que será referencia del módulo real y talón en consecuencia.

+0

Este enfoque funcionó muy bien para mí. En mi caso, agregué esto al html de la página del corredor de prueba -> map: {\t \t \t '*': {'Common/Modules/usefulModule': '/test/specs/common/usefulmodulemock.js'}} – Aligned

9

Puede usar testr.js para simular dependencias. Puede configurar testr para cargar las dependencias simuladas en lugar de las originales. He aquí un ejemplo de uso:

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

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

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

+2

Realmente quería que testr.js funcionara, pero aún no parece estar a la altura de la tarea. Al final voy con la solución de @Andreas Köberle, que agregará contextos anidados a mis pruebas (no es bonita) pero que funciona de manera consistente. Ojalá alguien pudiera enfocarse en resolver esta solución de una manera más elegante. Seguiré viendo testr.js y si/cuando funcione, hará el cambio. –

+0

@shioyama hola, gracias por los comentarios! Me gustaría echarle un vistazo a cómo has configurado testr.js dentro de tu stack de prueba. ¡Feliz de ayudarte a solucionar cualquier problema que puedas tener! También está la página Problemas de Github si quieres ingresar algo allí. Gracias, –

+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 es realmente bastante bueno y en hecho de acuerdo con cómo require.js estaba destinado a ser utilizado para burlarse/trocear. –

44

es posible que desee echa un vistazo a la nueva Squire.js lib

de los documentos:

Squire.js es ¡un inyector de dependencia para usuarios de Require.js para facilitar las dependencias de burla!

+5

Squire funcionó muy bien para mí. –

+2

¡Muy recomendable! Estoy actualizando mi código para usar squire.js y hasta ahora me está gustando mucho. Es un código muy simple, sin gran magia bajo el capó, pero hecho de una manera que es (relativamente) fácil de entender. –

+0

Tuve muchos problemas con Squire para realizar otras pruebas y no puedo recomendarlo. Recomendaría https://www.npmjs.com/package/requirejs-mock –

1

si quieres hacer algunas pruebas js de civil que aíslan una unidad, a continuación, sólo tiene que utilizar 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; 
2

Esta respuesta se basa en Andreas Köberle's answer.
No fue tan fácil para mí implementar y comprender su solución, así que lo explicaré con más detalle cómo funciona, y algunos escollos para evitar, con la esperanza de que ayudará a los futuros visitantes.

Así, en primer lugar la configuración:
estoy usando Karma como corredor de prueba y MochaJs como marco de pruebas.

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

TypeError: Cannot read property 'call' of undefined

RequireJs tiene la posibilidad de map ID del módulo a otro módulo IDS. También permite crear un require function que usa un different config que el require global.
Estas características son cruciales para que esta solución funcione.

Aquí está mi versión del código simulado, que incluye (mucho) comentarios (espero que sea comprensible). Lo envolví dentro de un módulo, para que las pruebas lo puedan requerir 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 escollo me encontré, que literalmente me costó horas, fue la creación de la configuración RequireJS. Traté de (profundamente) copiarlo, y solo anulo las propiedades necesarias (como el contexto o el mapa). ¡Esto no funciona! Copie solo el baseUrl, esto funciona bien.

Uso

Para usarlo, requieren en su prueba, crear la burla, y luego pasarlo a 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 completa:

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