Como simular dependências para testes de unidade no RequireJS?

127

Eu tenho um módulo AMD que quero testar, mas quero zombar de suas dependências em vez de carregar as dependências reais. Estou usando requirejs, e o código do meu módulo é algo como isto:

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

Como posso zombar hurpe durppoder efetivamente testar a unidade?

jergason
fonte
Estou apenas fazendo algumas coisas loucas de avaliação no node.js para zombar da definefunção. Existem algumas opções diferentes. Vou postar uma resposta na esperança de que seja útil.
27412 jergason
1
Para testes de unidade com o Jasmine, você também pode dar uma olhada rápida no Jasq . [Disclaimer: Eu estou mantendo a lib]
biril
1
Se você estiver testando no nó env, poderá usar o pacote require-mock . Ele permite que você zombar facilmente suas dependências, substituir módulos etc. Se você precisa env navegador com carga módulo assíncrono - você poderia tentar Squire.js
ValeriiVasin

Respostas:

64

Então, depois de ler este post, criei uma solução que usa a função de configuração requirejs para criar um novo contexto para o seu teste, onde você pode simplesmente zombar de suas dependências:

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

Portanto, ele cria um novo contexto em que as definições Hurpe Durpserão definidas pelos objetos que você passou para a função. O Math.random para o nome é talvez um pouco sujo, mas funciona. Porque, se você tiver vários testes, precisará criar um novo contexto para cada conjunto para evitar a reutilização de suas simulações ou carregá-las quando desejar o módulo real requirejs.

No seu caso, seria assim:

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

Então, eu estou usando essa abordagem na produção por um tempo e é realmente robusta.

Andreas Köberle
fonte
1
Gosto do que você está fazendo aqui ... especialmente porque você pode carregar um contexto diferente para cada teste. A única coisa que gostaria de poder mudar é que parece que só está funcionando se eu zombar de todas as dependências. Você conhece uma maneira de retornar os objetos simulados, se eles estiverem lá, mas recuar na recuperação do arquivo .js real, se um simulador não for fornecido? Eu tenho tentado cavar o código de exigência para descobrir isso, mas estou ficando um pouco perdido.
21912 Glen Hughes
5
Apenas zomba da dependência que você passa para a createContextfunção. Portanto, no seu caso, se você passar apenas {hurp: 'hurp'}para a função, o durparquivo será carregado como uma dependência normal.
Andreas Köberle 29/07
1
Estou usando isso no Rails (com jasminerice / phantomjs) e foi a melhor solução que encontrei para zombar do RequireJS.
Ben Anderson
13
+1 Não é bonito, mas de todas as soluções possíveis, essa parece ser a menos feia / confusa. Esse problema merece mais atenção.
31812 Chris Salzberg
1
Atualização: para qualquer pessoa que considere esta solução, sugiro verificar o squire.js ( github.com/iammerrick/Squire.js ) mencionado abaixo. É uma boa implementação de uma solução semelhante a esta, criando novos contextos onde quer que sejam necessários stubs.
precisa
44

convém verificar a nova lib do Squire.js

dos documentos:

O Squire.js é um injetor de dependência para usuários do Require.js, para facilitar as dependências de zombaria!

busticated
fonte
2
Fortemente recomendado! Estou atualizando meu código para usar o squire.js e até agora estou gostando muito. Código muito simples, sem muita mágica oculta, mas feito de uma maneira que é (relativamente) fácil de entender.
precisa
1
Eu tive muitos problemas com o escudeiro afetando outros testes e não posso recomendá-lo. Eu recomendaria npmjs.com/package/requirejs-mock
Jeff Whiting
17

Eu encontrei três soluções diferentes para esse problema, nenhuma delas agradável.

Definindo dependências em linha

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

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

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

Fugly. Você precisa desordenar seus testes com muitos clichês da AMD.

Carregando dependências simuladas de caminhos diferentes

Isso envolve o uso de um arquivo config.js separado para definir caminhos para cada uma das dependências que apontam para zombarias, em vez das dependências originais. Isso também é feio, exigindo a criação de toneladas de arquivos de teste e arquivos de configurações.

Fake It In Node

Esta é a minha solução atual, mas ainda é terrível.

Você cria sua própria definefunção para fornecer suas próprias simulações ao módulo e colocar seus testes no retorno de chamada. Então você é evalo módulo para executar seus testes, da seguinte forma:

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 é a minha solução preferida. Parece um pouco de mágica, mas tem alguns benefícios.

  1. Execute seus testes no nó, para não mexer com a automação do navegador.
  2. Menos necessidade de clichês confusos da AMD em seus testes.
  3. Você começa a usar a evalraiva e imagina Crockford explodindo de raiva.

Ainda tem algumas desvantagens, obviamente.

  1. Como você está testando no nó, não pode fazer nada com eventos do navegador ou manipulação do DOM. Apenas bom para testar a lógica.
  2. Ainda um pouco desajeitado de configurar. Você precisa zombar de definetodos os testes, pois é aí que seus testes realmente são executados.

Estou trabalhando em um executor de teste para fornecer uma sintaxe melhor para esse tipo de coisa, mas ainda não tenho uma boa solução para o problema 1.

Conclusão

Zombar deps em requirejs é uma droga. Eu encontrei uma maneira que funciona, mas ainda não estou muito feliz com isso. Entre em contato se tiver alguma idéia melhor.

jergason
fonte
15

Existe uma config.mapopção http://requirejs.org/docs/api.html#config-map .

Sobre como usá-lo:

  1. Definir módulo normal;
  2. Definir módulo stub;
  3. Configure o RequireJS de forma exponencial

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

Nesse caso, para código normal e de teste, você pode usar o foomódulo, que será uma referência real do módulo e o stub adequadamente.

Artem Oboturov
fonte
Essa abordagem funcionou muito bem para mim. No meu caso, adicionei isso ao html da página do executor de testes -> map: {'*': {'Common / Modules / usefulModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
alinhado
9

Você pode usar o testr.js para simular dependências. Você pode configurar o testr para carregar as dependências simuladas em vez das dependentes originais. Aqui está um exemplo de uso:

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

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

Confira também: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

janith
fonte
2
Eu realmente queria que o testr.js funcionasse, mas ainda não parece bem a tarefa. No final, vou usar a solução de @Andreas Köberle, que adiciona contextos aninhados aos meus testes (não muito bonitos), mas que sempre funciona. Eu gostaria que alguém pudesse se concentrar em resolver esta solução de uma maneira mais elegante. Continuarei assistindo testr.js e se / quando funcionar, fará a troca.
31812 Chris Salzberg
@shioyama oi, obrigado pelo feedback! Gostaria de ver como você configurou o testr.js na sua pilha de testes. É um prazer ajudá-lo a corrigir quaisquer problemas que possa estar tendo! Há também a página Problemas do github, se você deseja registrar algo lá. Obrigado,
Matty F
1
@ MattyF desculpe, eu nem me lembro agora qual foi o motivo exato pelo qual testr.js não funcionou para mim, mas cheguei à conclusão de que o uso de contextos extras é realmente muito bom e, de fato, está de acordo com como o require.js foi projetado para ser usado para zombaria / stub.
22813 Chris Salzberg
2

Esta resposta é baseada na resposta de Andreas Köberle .
Não foi tão fácil para mim implementar e entender sua solução, por isso vou explicar com mais detalhes como ela funciona e algumas armadilhas a serem evitadas, esperando que ajude futuros visitantes.

Então, primeiro de tudo a configuração:
eu estou usando Karma como executor de teste e MochaJs como estrutura de teste.

Usando algo como Squire não funcionou para mim, por algum motivo, quando o usei, a estrutura de teste gerou erros:

TypeError: Não é possível ler a propriedade 'call' de undefined

RequireJs tem a possibilidade de mapear IDs de módulo para outros IDs de módulo. Também permite criar uma requirefunção que usa uma configuração diferente da globalrequire .
Esses recursos são cruciais para que esta solução funcione.

Aqui está a minha versão do código simulado, incluindo (muitos) comentários (espero que seja compreensível). Coloquei-o dentro de um módulo, para que os testes possam exigir com facilidade.

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

o maior armadilha que encontrei, que literalmente me custou horas, foi criar a configuração do RequireJs. Tentei copiá-lo (em profundidade) e apenas substituí as propriedades necessárias (como contexto ou mapa). Isso não funciona! Apenas copie o baseUrl, isso funciona bem.

Uso

Para usá-lo, exija-o em seu teste, crie as simulações e depois passe-o para createMockRequire. Por exemplo:

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

E aqui um exemplo de um arquivo de teste 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
fonte
0

se você quiser fazer alguns testes js simples que isolam uma unidade, basta usar este snippet:

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;
user3033599
fonte