teste de unidade de funções privadas com mocha e node.js

131

Estou usando o mocha para fazer o teste de unidade de um aplicativo escrito para node.js

Gostaria de saber se é possível realizar funções de teste de unidade que não foram exportadas em um módulo.

Exemplo:

Eu tenho muitas funções definidas assim em foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

e algumas funções exportadas como públicas:

exports.public_foobar3 = function(){
    ...
}

O caso de teste está estruturado da seguinte maneira:

describe("private_foobar1", function() {
    it("should do stuff", function(done) {
        var stuff = foobar.private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....

Obviamente, isso não funciona, pois private_foobar1não é exportado.

Qual é a maneira correta de testar métodos particulares? O mocha possui alguns métodos internos para fazer isso?

fstab
fonte

Respostas:

64

Se a função não for exportada pelo módulo, ela não poderá ser chamada pelo código de teste fora do módulo. Isso se deve à maneira como o JavaScript funciona, e o Mocha não pode contornar isso por si só.

Nos poucos casos em que determinei que testar uma função privada é a coisa certa a fazer, o que fiz foi definir alguma variável de ambiente que meu módulo verifica para determinar se está sendo executado em uma configuração de teste ou não. Se ele for executado na configuração de teste, ele exporta funções adicionais que eu posso chamar durante o teste.

A palavra "ambiente" é pouco usada aqui. Pode significar verificação process.envou outra coisa que possa se comunicar com o módulo "você está sendo testado agora". As instâncias em que tive que fazer isso estavam em um ambiente RequireJS e usei module.configpara esse fim.

Louis
fonte
2
A exportação condicional de valores não parece ser compatível com os módulos ES6. Estou recebendoSyntaxError: 'import' and 'export' may only appear at the top level
dia
1
@aij yes devido às exportações estáticas do ES6 que você não pode usar import, exportdentro de um bloco. Eventualmente, você poderá realizar esse tipo de coisa no ES6 com o carregador do sistema. Uma maneira de contornar isso agora é usar module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')e armazenar suas diferenças de código es6 nesses arquivos.
Cchamberlain
2
Eu acho que se você tiver cobertura total, estará testando todas as suas funções privadas, independentemente de você as ter exposto ou não.
Ziggy
1
@aij Você pode exportar condicionalmente ... veja esta resposta: stackoverflow.com/questions/39583958/…
#
187

Confira o módulo de religação . Ele permite que você obtenha (e manipule) variáveis ​​e funções privadas dentro de um módulo.

Portanto, no seu caso, o uso seria algo como:

var rewire = require('rewire'),
    foobar = rewire('./foobar'); // Bring your module in with rewire

describe("private_foobar1", function() {

    // Use the special '__get__' accessor to get your private function.
    var private_foobar1 = foobar.__get__('private_foobar1');

    it("should do stuff", function(done) {
        var stuff = private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....
barwin
fonte
3
@Jaro A maior parte do meu código está na forma de módulos AMD, que a religação é incapaz de manipular (porque os módulos AMD são funções, mas a religação não pode manipular "variáveis ​​dentro de funções"). Ou é transpilado, outro cenário que a religação não pode lidar. Na verdade, as pessoas que vão olhar para religar fariam bem em primeiro ler as limitações (vinculadas anteriormente) antes de tentarem usá-la. Não tenho um único aplicativo que a) precise exportar coisas "particulares" eb) não tenha uma limitação de religação.
Louis
1
Apenas um pequeno ponto, a cobertura do código pode falhar em pegar testes escritos assim. Pelo menos é o que eu vi usando a ferramenta de cobertura incorporada do Jest.
precisa
O Rewire também não funciona bem com a ferramenta de zombaria automática da brincadeira. Ainda estou procurando uma maneira de aproveitar os benefícios do gracejo e acessar alguns vars privados.
precisa saber é o seguinte
Então, tentei fazer esse trabalho, mas estou usando texto datilografado, o que acho que está causando esse problema. Basicamente, eu recebo o seguinte erro: Cannot find module '../../package' from 'node.js'. Alguém familiarizado com isso?
clu
religação está funcionando bem no .ts, typescripteu corro usando ts-node @clu
Muthukumar selvaraj
24

Aqui está um fluxo de trabalho realmente bom para testar seus métodos particulares, explicados por Philip Walton, engenheiro do Google em seu blog.

Princípio

  • Escreva seu código normalmente
  • Vincule seus métodos particulares ao objeto em um bloco de código separado, marque-o com um _por exemplo
  • Coloque esse bloco de código nos comentários iniciais e finais

Em seguida, use uma tarefa de compilação ou seu próprio sistema de compilação (por exemplo, código de faixa de resmungo) para retirar esse bloco para compilações de produção.

Suas construções de testes têm acesso à sua API privada e suas construções de produção não.

Snippet

Escreva seu código como este:

var myModule = (function() {

  function foo() {
    // private function `foo` inside closure
    return "foo"
  }

  var api = {
    bar: function() {
      // public function `bar` returned from closure
      return "bar"
    }
  }

  /* test-code */
  api._foo = foo
  /* end-test-code */

  return api
}())

E suas tarefas difíceis assim

grunt.registerTask("test", [
  "concat",
  "jshint",
  "jasmine"
])
grunt.registerTask("deploy", [
  "concat",
  "strip-code",
  "jshint",
  "uglify"
])

Mais profundo

Em um artigo posterior , ele explica o "porquê" de "testar métodos privados"

Rémi Becheras
fonte
1
Também encontrado um plugin webkit que parece que pode suportar um fluxo de trabalho semelhante: Webpack-strip-block
JRulle
21

Se você preferir manter as coisas simples, exporte também os membros privados, mas claramente separados da API pública com alguma convenção, por exemplo, prefixe-os com um _ou aninhe-os em um único objeto privado .

var privateWorker = function() {
    return 1
}

var doSomething = function() {
    return privateWorker()
}

module.exports = {
    doSomething: doSomething,
    _privateWorker: privateWorker
}
Famousgarkin
fonte
7
Eu fiz isso nos casos em que o módulo inteiro é realmente destinado a ser privado e não para consumo comum. Mas para os módulos de uso geral, prefiro expor o que preciso para testar apenas quando o código está sendo testado. É verdade que, em última análise, não há nada que impeça alguém de acessar o material privado falsificando um ambiente de teste, mas quando alguém está depurando em seu próprio aplicativo, prefiro que não vejam os símbolos que não precisam ser parte da API pública. Dessa forma, não há tentação imediata de abusar da API para fins para os quais não foi projetada.
Louis
2
você também pode usar sintaxe aninhada {... private : {trabalhador: Trabalhador}}
Jason
2
Se o módulo tiver todas as funções puras, não vejo desvantagem em fazer isso. Se você está mantendo e mutando o estado, então cuidado ...
Ziggy
5

Criei um pacote npm para esse fim que você pode achar útil: require-from

Basicamente, você expõe métodos não públicos por:

module.testExports = {
    private_foobar1: private_foobar1,
    private_foobar2: private_foobar2,
    ...
}

nota: testExports pode ser qualquer nome válido que você queira, exceto é exportsclaro.

E de outro módulo:

var requireFrom = require('require-from');
var private_foobar1 = requireFrom('testExports', './path-to-module').private_foobar1;
DEADB17
fonte
1
Não vejo vantagem prática nesse método. Não torna os símbolos "particulares" mais privados. (Qualquer pessoa pode chamar requireFromcom os parâmetros corretos.) Além disso, se o módulo textExportsé carregado por uma requirechamada antes de carregá- requireFromla, requireFromretornará undefined. (Acabei de testar.) Embora muitas vezes seja possível controlar a ordem de carga dos módulos, nem sempre é prático. (Como evidenciado por algumas perguntas do Mocha sobre SO). Essa solução também geralmente não funciona com módulos do tipo AMD. (Eu carrego módulos da AMD no Node diariamente para teste.) #
Louis
Não deve funcionar com módulos AMD! O Node.js usa o common.js e, se você o alterar para usar a AMD, estará fazendo isso fora da norma.
22415 jemiloii
@JemiloII Centenas de desenvolvedores usam o Node.js diariamente para testar os módulos da AMD. Não há nada "fora da norma" ao fazer isso. O máximo que você pode dizer é que o Node.js não vem com um carregador AMD, mas isso não significa muito, já que o Node fornece ganchos explícitos para estender seu carregador para carregar qualquer formato que os desenvolvedores desejem desenvolver.
Louis
Está fora da norma. Se você precisar incluir manualmente um amd loader, essa não é a norma para o node.js. Raramente vejo a AMD para o código node.js. Vou vê-lo para o navegador, mas nó. Não. Não estou dizendo que não está sendo feito, apenas a pergunta e a resposta que estamos comentando não dizem nada sobre os módulos amd. Portanto, sem que alguém indique que está usando um carregador amd, as exportações de nó não devem funcionar com o amd. Embora eu queira observar, o commonjs pode estar saindo com as exportações do es6. Só espero que um dia todos possamos usar apenas um método de exportação.
jemiloii
4

Eu adicionei uma função extra com o nome Internal () e retorno todas as funções privadas a partir daí. Essa função Internal () é exportada. Exemplo:

function Internal () {
  return { Private_Function1, Private_Function2, Private_Function2}
}

// Exports --------------------------
module.exports = { PublicFunction1, PublicFunction2, Internal }

Você pode chamar as funções internas assim:

let test = require('.....')
test.Internal().Private_Function1()

Eu gosto mais desta solução porque:

  • somente uma função Internal () é sempre exportada. Essa função Internal () é sempre usada para testar funções privadas.
  • É simples de implementar
  • Baixo impacto no código de produção (apenas uma função extra)
Perez Lamed van Niekerk
fonte
2

Eu segui a resposta @barwin e verifiquei como os testes de unidade podem ser feitos com o módulo de religação . Posso confirmar que esta solução simplesmente funciona.

O módulo deve ser exigido em duas partes - pública e privada. Para funções públicas, você pode fazer isso da maneira padrão:

const { public_foobar3 } = require('./foobar');

Para escopo privado:

const privateFoobar = require('rewire')('./foobar');
const private_foobar1 = privateFoobar .__get__('private_foobar1');
const private_foobar2 = privateFoobar .__get__('private_foobar2');

Para saber mais sobre o assunto, criei um exemplo de trabalho com teste completo do módulo, o teste inclui escopo público e privado.

Para obter mais informações, recomendamos que você verifique o artigo ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ) descrevendo completamente o assunto, incluindo exemplos de código.

Maciej Sikora
fonte
2

Sei que essa não é necessariamente a resposta que você está procurando, mas o que descobri é que, na maioria das vezes, se uma função privada vale a pena testar, vale a pena estar em seu próprio arquivo.

Por exemplo, em vez de ter métodos privados no mesmo arquivo que os públicos, como este ...

src / thing / PublicInterface.js


function helper1 (x) {
    return 2 * x;
}

function helper2 (x) {
    return 3 * x;
}

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

... você divide assim:

src / thing / PublicInterface.js

import {helper1} from './internal/helper1.js';
import {helper2} from './internal/helper2.js';

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

src / coisa / interno / helper1.js

export function helper1 (x) {
    return 2 * x;
}

src / coisa / interno / helper2.js

export function helper2 (x) {
    return 3 * x;
}

Dessa forma, você pode facilmente testar helper1e helper2como está, sem usar o Rewire e outras "mágicas" (que, eu descobri, têm seus próprios pontos problemáticos durante a depuração, ou quando você tenta avançar para o TypeScript, sem mencionar os mais pobres compreensibilidade para novos colegas). E eles estarem em uma subpasta chamada internal, ou algo assim, ajudará a evitar o uso acidental deles em locais indesejados.


PS: Outro problema comum com os métodos "privados" é que se pretende testar publicMethod1e publicMethod2e zombar dos ajudantes, mais uma vez, você normalmente precisa de algo como Rewire para fazer isso. No entanto, se eles estiverem em arquivos separados, é possível usar o Proxyquire para fazê-lo, o que, diferentemente do Rewire, não precisa de alterações no processo de compilação, é fácil de ler e depurar e funciona bem mesmo com o TypeScript.

Dániel Kis-Nagy
fonte
1

Para disponibilizar métodos particulares para teste, eu faço o seguinte:

const _myPrivateMethod: () => {};

const methods = {
    myPublicMethod1: () => {},
    myPublicMethod2: () => {},
}

if (process.env.NODE_ENV === 'test') {
    methods._myPrivateMethod = _myPrivateMethod;
}

module.exports = methods;
MFB
fonte