Como testar a unidade de um módulo Node.js. que requer outros módulos e como zombar da função de exigência global?


156

Este é um exemplo trivial que ilustra o cerne do meu problema:

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

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

module.exports = underTest;

Estou tentando escrever um teste de unidade para este código. Como posso zombar do requisito do innerLibsem zombar requiretotalmente da função?

Então, sou eu que estou tentando zombar do global requiree descobrindo que ele não funcionará nem para fazer isso:

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

O problema é que a requirefunção dentro do underTest.jsarquivo não foi realmente zombada. Ainda aponta para a requirefunção global . Parece que só posso zombar da requirefunção no mesmo arquivo em que estou zombando. Se eu usar o global requirepara incluir qualquer coisa, mesmo depois de substituir a cópia local, os arquivos solicitados ainda terão o requirereferência global .


você tem que substituir global.require. As variáveis ​​gravam modulepor padrão, pois os módulos têm escopo definido no módulo.
Raynos

@ Raynos Como eu faria isso? global.require é indefinido? Mesmo se eu a substituísse por minha própria função, outras funções nunca usariam isso?
HMR

Respostas:


175

Agora você pode!

I publicado proxyquire que vai cuidar de substituir exigem o mundial dentro do seu módulo enquanto estiver testando ele.

Isso significa que você não precisa de alterações no seu código para injetar zombarias nos módulos necessários.

O Proxyquire possui uma API muito simples, que permite resolver o módulo que você está tentando testar e repassar zombarias / stubs para os módulos necessários em uma única etapa.

A @Raynos está certa de que, tradicionalmente, você tinha que recorrer a soluções não muito ideais para conseguir isso ou fazer um desenvolvimento de baixo para cima

Qual é a principal razão pela qual eu criei o proxyquire - para permitir o desenvolvimento orientado a testes de cima para baixo sem qualquer aborrecimento.

Dê uma olhada na documentação e nos exemplos para avaliar se ela atenderá às suas necessidades.


5
Eu uso proxyquire e não posso dizer coisas boas o suficiente. Isso me salvou! Fui encarregado de escrever testes de nós de jasmim para um aplicativo desenvolvido no appcelerator Titanium, que força alguns módulos a serem caminhos absolutos e muitas dependências circulares. proxyquire, deixe-me acabar com essas lacunas e zombar do lixo que eu não precisava para cada teste. (Explicado aqui ). Muito obrigado!
Sukima 5/02

Feliz em saber que proxyquire ajudou a testar seu código corretamente :)
Thorsten Lorenz

1
muito legal @ThorstenLorenz, eu vou def. estar usando proxyquire!
Bevacqua

Fantástico! Quando vi a resposta aceita que "você não pode", pensei "Oh Deus, sério ?!" mas isso realmente salvou.
usar o seguinte

3
Para aqueles que usam o Webpack, não gaste tempo pesquisando proxyquir. Não suporta Webpack. Estou pesquisando no injetor -carregador ( github.com/plasticine/inject-loader ).
Artif3x

116

Uma opção melhor nesse caso é zombar dos métodos do módulo retornado.

Para melhor ou pior, a maioria dos módulos node.js. são singletons; dois pedaços de código que requerem () o mesmo módulo obtêm a mesma referência a esse módulo.

Você pode aproveitar isso e usar algo como sinon para zombar dos itens necessários. teste mocha a seguir:

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

O Sinon tem uma boa integração com o chai para fazer afirmações, e eu escrevi um módulo para integrar o sinon ao mocha para facilitar a limpeza de espiões / stub (para evitar a poluição nos testes).

Observe que underTest não pode ser ridicularizado da mesma maneira, pois underTest retorna apenas uma função.

Outra opção é usar zombarias Jest. Acompanhamento na página deles


1
Infelizmente, NÃO é garantido que os módulos node.js. sejam singletons, como explicado aqui: justjs.com/posts/…
FrontierPsycho

4
@FrontierPsycho algumas coisas: primeiro, no que diz respeito aos testes, o artigo é irrelevante. Enquanto você estiver testando suas dependências (e não dependências de dependências), todo o seu código receberá o mesmo objeto quando você require('some_module'), porque todo o seu código compartilha o mesmo diretório node_modules. Segundo, o artigo está confundindo o espaço de nome com os singletons, o que é uma espécie de ortogonal. Terceiro, esse artigo é muito antigo (no que diz respeito ao node.js), então o que poderia ter sido válido naquele dia talvez não seja válido agora.
Elliot Foster

2
Hum. A menos que um de nós desenterre um código que comprove um ponto ou outro, eu usaria sua solução de injeção de dependência ou simplesmente passaria objetos, é mais seguro e mais à prova do futuro.
FrontierPsycho

1
Não tenho certeza do que você está pedindo para ser provado. A natureza singleton (em cache) dos módulos de nós é comumente entendida. A injeção de dependência, embora seja uma boa rota, pode ser uma quantidade razoável de mais placas de caldeira e mais código. A DI é mais comum em linguagens de tipo estaticamente, onde é mais difícil inserir espiões / stubs / zombarias no seu código dinamicamente. Vários projetos que fiz nos últimos três anos usam o método descrito na minha resposta acima. É o método mais fácil de todos, embora eu o use com moderação.
Elliot Foster

1
Eu sugiro que você leia sobre sinon.js. Se você estiver usando sinon (como no exemplo acima), você teria quer innerLib.toCrazyCrap.restore()e restub, ou ligue sinon via sinon.stub(innerLib, 'toCrazyCrap')que lhe permite mudar a forma como se comporta stub: innerLib.toCrazyCrap.returns(false). Além disso, a religação parece ser muito parecida com a proxyquireextensão acima.
Elliot Foster

11

Eu uso mock-exigem . Certifique-se de definir suas zombarias antes requirede testar o módulo.


Também é bom executar stop (<file>) ou stopAll () imediatamente, para que você não obtenha um arquivo em cache em um teste em que não deseja a simulação.
Justin Kruse

1
Isso ajudou muito.
wallop

2

Zombar requireparece um truque desagradável para mim. Eu pessoalmente tentaria evitá-lo e refatorar o código para torná-lo mais testável. Existem várias abordagens para lidar com dependências.

1) passar dependências como argumentos

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

Isso tornará o código universalmente testável. A desvantagem é que você precisa repassar dependências, o que pode tornar o código mais complicado.

2) implemente o módulo como uma classe e use métodos / propriedades de classe para obter dependências

(Este é um exemplo artificial, em que o uso da classe não é razoável, mas transmite a ideia) (exemplo ES6)

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

class underTestClass {
    getInnerLib () {
        return innerLib
    }

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

Agora você pode facilmente stub getInnerLibmétodo para testar seu código. O código se torna mais detalhado, mas também mais fácil de testar.


1
Eu não acho que seja hacky como você presume ... essa é a essência da zombaria. Zombar das dependências necessárias simplifica as coisas, dando controle ao desenvolvedor sem alterar a estrutura do código. Seus métodos são muito detalhados e, portanto, difíceis de raciocinar. Eu escolho requisitar proxy ou exigir simulado sobre isso; Não vejo nenhum problema aqui. O código é limpo e fácil de raciocinar e lembre-se de que a maioria das pessoas que lê esse código já escreveu um código que você deseja que elas complicem. Se essas bibliotecas são hackish, zombar e stubbing também é hackish por sua definição e deve ser interrompido.
Emmanuel Mahuni

1
O problema com a abordagem no.1 é que você está passando detalhes de implementação interna na pilha. Com várias camadas, torna-se muito mais complicado ser consumidor do seu módulo. Ele pode funcionar com uma abordagem semelhante a um contêiner do IOC, para que as dependências sejam injetadas automaticamente para você; no entanto, parece que já que já temos dependências injetadas nos módulos de nó por meio da declaração de importações, faz sentido poder zombar delas nesse nível .
magritte

1) Este simplesmente move o problema para outro arquivo 2) ainda carrega o outro módulo e, portanto, impõe sobrecarga de desempenho e, possivelmente, provoca efeitos colaterais (como o popular colorsmódulo que messes com String.prototype)
ThomasR

2

Se você já usou o gracejo, provavelmente conhece o recurso simulado do gracejo.

Usando "jest.mock (...)", você pode simplesmente especificar a string que ocorreria em uma instrução de requisição em seu código em algum lugar e sempre que um módulo for necessário usando essa string, um objeto simulado será retornado.

Por exemplo

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

substituiria completamente todas as importações / exigências de "firebase-admin" pelo objeto que você retornou dessa função "factory".

Bem, você pode fazer isso ao usar o gracejo, porque o gracejo cria um tempo de execução em torno de cada módulo executado e injeta uma versão "viciada" do requisito no módulo, mas você não seria capaz de fazer isso sem o gracejo.

Eu tentei conseguir isso com mock-require, mas para mim não funcionou para níveis aninhados na minha fonte. Dê uma olhada no seguinte problema no github: mock-require nem sempre chamado com Mocha .

Para resolver isso, criei dois módulos npm que você pode usar para alcançar o que deseja.

Você precisa de um plug-in babel e de um mocker de módulo.

No seu .babelrc, use o plug-in babel-plugin-mock-require com as seguintes opções:

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

e no seu arquivo de teste, use o módulo jestlike-mock da seguinte maneira:

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

O jestlike-mockmódulo ainda é muito rudimentar e não possui muita documentação, mas também não há muito código. Aprecio todos os PRs para um conjunto de recursos mais completo. O objetivo seria recriar todo o recurso "jest.mock".

Para ver como o jest implementa, é possível procurar o código no pacote "jest-runtime". Veja https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734, por exemplo, aqui eles geram um "automock" de um módulo.

Espero que ajude ;)


1

Você não pode. Você precisa construir seu conjunto de testes unitários para que os módulos mais baixos sejam testados primeiro e que os módulos de nível superior que requerem módulos sejam testados posteriormente.

Você também deve assumir que qualquer código de terceiros e o próprio node.js sejam bem testados.

Presumo que você verá estruturas de simulação chegando em um futuro próximo que sobrescrevem global.require

Se você realmente precisa injetar uma simulação, pode alterar seu código para expor o escopo 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());
}

Esteja ciente de que isso se expõe .__moduleà sua API e qualquer código pode acessar o escopo modular por seu próprio perigo.


2
Assumir que o código de terceiros foi bem testado não é uma ótima maneira de trabalhar com a IMO.
henry.oswald

5
@ Beck é uma ótima maneira de trabalhar. Obriga-nos a única obra com código de terceiros de alta qualidade ou gravar todas as peças do seu código para que cada dependência é bem testados
Raynos

Ok, pensei que você estava se referindo a não fazer testes de integração entre seu código e o código de terceiros. Acordado.
Henry.oswald 29/07/2012

1
Um "conjunto de testes de unidade" é apenas uma coleção de testes de unidade, mas os testes de unidade devem ser independentes um do outro, portanto, a unidade em teste de unidade. Para serem utilizáveis, os testes de unidade devem ser rápidos e independentes, para que você possa ver claramente onde o código está quebrado quando um teste de unidade falha.
Andreas Berheim Brudin

Isso não funcionou para mim. O objeto do módulo não expõe o "var innerLib ..." etc.
AnitKryst

1

Você pode usar a biblioteca de zombaria :

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 simples para simular módulos para os curiosos

Observe as partes em que você manipula o método require.cachee observe require.resolve, pois esse é o molho secreto.

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 = {}
  }
}

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

MAS ... proxyquire é bastante impressionante e você deve usá-lo. Ele mantém as substituições necessárias localizadas apenas nos testes e eu recomendo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.