pruebas unitarias de funciones privadas con mocha y node.js

131

Estoy usando mocha para probar una aplicación escrita para node.js

Me pregunto si es posible unir las funciones de prueba que no se han exportado en un módulo.

Ejemplo:

Tengo muchas funciones definidas así en foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

y algunas funciones exportadas como públicas:

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

El caso de prueba se estructura de la siguiente manera:

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

Obviamente esto no funciona, ya private_foobar1que no se exporta.

¿Cuál es la forma correcta de probar los métodos privados? ¿Tiene mocha algunos métodos incorporados para hacerlo?

fstab
fuente

Respuestas:

64

Si el módulo no exporta la función, no se puede invocar mediante código de prueba fuera del módulo. Eso se debe a cómo funciona JavaScript, y Mocha no puede evitar esto por sí solo.

En los pocos casos en que determiné que probar una función privada es lo correcto, lo que hice fue establecer alguna variable de entorno que mi módulo verifica para determinar si se está ejecutando en una configuración de prueba o no. Si se ejecuta en la configuración de prueba, exporta funciones adicionales a las que puedo llamar durante la prueba.

La palabra "medio ambiente" se usa libremente aquí. Puede significar una comprobación process.envo algo más que pueda comunicarse con el módulo "estás siendo probado ahora". Las instancias donde tuve que hacer esto fueron en un entorno RequireJS, y lo he usado module.configpara este propósito.

Louis
fuente
2
Exportar condicionalmente los valores no parece ser compatible con los módulos ES6. Me estoy poniendoSyntaxError: 'import' and 'export' may only appear at the top level
aij
1
@aij sí debido a las exportaciones estáticas de ES6 que no puede usar import, exportdentro de un bloque. Eventualmente podrá lograr este tipo de cosas en ES6 con el cargador del sistema. Una forma de module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')evitarlo ahora es usar y almacenar sus diferencias de código es6 en esos archivos respectivos.
cchamberlain 01 de
2
Supongo que si tiene cobertura total, está probando todas sus funciones privadas, ya sea que las haya expuesto o no.
Ziggy
1
@aij Puedes exportar condicionalmente ... mira esta respuesta: stackoverflow.com/questions/39583958/…
RayLoveless
187

Echa un vistazo al módulo de recableado . Le permite obtener (y manipular) variables y funciones privadas dentro de un módulo.

Entonces, en su caso, el uso sería 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
fuente
3
@Jaro La mayor parte de mi código está en forma de módulos AMD, que el cableado no puede manejar (porque los módulos AMD son funciones pero el cableado no puede manejar "variables dentro de las funciones"). O se transpila, otro escenario que el cableado no puede manejar. En realidad, las personas que van a ver el rewire harían bien en leer primero las limitaciones (vinculadas anteriormente) antes de intentar usarlo. No tengo una sola aplicación que a) necesite exportar cosas "privadas" yb) no se encuentre con una limitación de recableado.
Louis
1
Solo un pequeño punto, la cobertura del código puede fallar al recoger pruebas escritas de esta manera. Al menos eso es lo que he visto usando la herramienta de cobertura incorporada de Jest.
Mike Stead
Rewire tampoco funciona bien con la herramienta de burla automática de jest. Todavía estoy buscando una manera de aprovechar los beneficios de Jest y acceder a algunos vars privados.
btburton42
Así que intenté hacer que esto funcionara, pero estoy usando el mecanografiado, que supongo que está causando este problema. Básicamente me sale el siguiente error: Cannot find module '../../package' from 'node.js'. Alguien familiarizado con esto?
clu
rewire funciona bien en .ts, typescriptejecuto usando ts-node @clu
muthukumar selvaraj
24

Aquí hay un flujo de trabajo realmente bueno para probar sus métodos privados explicado por Philip Walton, un ingeniero de Google en su blog.

Principio

  • Escribe tu código normalmente
  • Unen sus métodos particulares al objeto en un bloque de código independiente, marcan su por una _, por ejemplo,
  • Rodea ese bloque de código con comentarios iniciales y finales

Luego use una tarea de compilación o su propio sistema de compilación (por ejemplo, grunt-strip-code) para eliminar este bloque para compilaciones de producción.

Sus compilaciones de prueba tienen acceso a su API privada, y sus compilaciones de producción no.

Retazo

Escribe tu 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
}())

Y tus gruñidos como ese

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

Mas profundo

En un artículo posterior , explica el "por qué" de "probar métodos privados"

Rémi Becheras
fuente
1
También encontré un complemento de webkit que parece que puede soportar un flujo de trabajo similar: webpack-strip-block
JRulle
21

Si prefiere que sea simple, simplemente exporte los miembros privados también, pero claramente separados de la API pública con alguna convención, por ejemplo, prefije con _o anidelos bajo un solo objeto privado .

var privateWorker = function() {
    return 1
}

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

module.exports = {
    doSomething: doSomething,
    _privateWorker: privateWorker
}
calabaza famosa
fuente
77
Lo he hecho en los casos en que todo el módulo realmente está destinado a ser privado y no para el consumo común. Pero para los módulos de uso general prefiero exponer lo que necesito para probar solo cuando se prueba el código. Es cierto que, en última instancia, no hay nada que impida que alguien acceda a lo privado fingiendo un entorno de prueba, pero cuando uno está depurando en su propia aplicación, prefiero que no vean los símbolos que no necesitan ser parte de la API pública. De esta manera, no existe la tentación inmediata de abusar de la API para fines para los que no está diseñada.
Louis
2
también puede usar la sintaxis anidada {... privado : {trabajador: trabajador}}
Jason
2
Si el módulo es todas funciones puras, entonces no veo ningún inconveniente en hacer esto. Si está manteniendo y mutando el estado, entonces tenga cuidado ...
Ziggy
5

Hice un paquete npm para este propósito que puede resultarle útil: require-from

Básicamente expones métodos no públicos mediante:

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

nota: testExports puede ser cualquier nombre válido que desee, excepto, exportspor supuesto.

Y de otro módulo:

var requireFrom = require('require-from');
var private_foobar1 = requireFrom('testExports', './path-to-module').private_foobar1;
DEADB17
fuente
1
No veo ninguna ventaja práctica para este método. No hace que los símbolos "privados" sean más privados. (Cualquiera puede llamar requireFromcon los parámetros correctos). Además, si el módulo con textExportses cargado por una requirellamada antes de cargarlo requireFrom, requireFromregresará undefined. (Lo acabo de probar). Si bien a menudo es posible controlar el orden de carga de los módulos, no siempre es práctico. (Como lo demuestran algunas preguntas de Mocha sobre SO). Esta solución tampoco suele funcionar con módulos de tipo AMD. (Cargo módulos AMD en Node diariamente para realizar pruebas).
Louis
¡No debería funcionar con módulos AMD! Node.js usa common.js y si lo cambia para usar AMD, entonces lo está haciendo fuera de la norma.
jemiloii
@JemiloII Cientos de desarrolladores usan Node.js diariamente para probar los módulos AMD. No hay nada "fuera de lo normal" al hacer eso. Lo máximo que puede decir es que Node.js no viene con un cargador AMD, pero esto no dice mucho, ya que Node proporciona ganchos explícitos para extender su cargador para cargar cualquier formato que los desarrolladores deseen desarrollar.
Louis
Está fuera de la norma. Si tiene que incluir manualmente un cargador AMD, no es la norma para node.js. Raramente veo AMD para el código node.js. Lo veré para el navegador, pero nodo. No. No digo que no se esté haciendo, solo la pregunta y esta respuesta que estamos comentando no dicen nada sobre los módulos amd. Por lo tanto, sin que nadie indique que están usando un cargador AMD, las exportaciones de nodos no deberían funcionar con AMD. Aunque quiero señalar, commonjs podría estar saliendo con las exportaciones de es6. Solo espero que algún día todos podamos usar un solo método de exportación.
jemiloii
4

He agregado una función adicional que denomino Internal () y devuelvo todas las funciones privadas desde allí. Esta función interna () se exporta. Ejemplo:

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

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

Puede llamar a las funciones internas de esta manera:

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

Esta solución me gusta más porque:

  • solo se exporta una función interna () . Esta función interna () siempre se usa para probar funciones privadas.
  • Es simple de implementar
  • Bajo impacto en el código de producción (solo una función adicional)
Pérez Lamed van Niekerk
fuente
2

Seguí @barwin respuesta y comprobado cómo las pruebas unitarias se pueden hacer con recableado módulo. Puedo confirmar que esta solución simplemente funciona.

El módulo debe requerirse en dos partes: una pública y una privada. Para funciones públicas, puede hacerlo de manera estándar:

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

Para ámbito privado:

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

Para saber más sobre el tema, creé un ejemplo de trabajo con pruebas de módulo completo, las pruebas incluyen alcance privado y público.

Para obtener más información, le recomiendo que consulte el artículo ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ) que describe completamente el tema, incluye ejemplos de código.

Maciej Sikora
fuente
2

Sé que esta no es necesariamente la respuesta que está buscando, pero lo que he encontrado es que la mayoría de las veces si vale la pena probar una función privada, vale la pena estar en su propio archivo.

Por ejemplo, en lugar de tener métodos privados en el mismo archivo que los 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);
}

... lo dividiste así:

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 / thing / internal / helper1.js

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

src / thing / internal / helper2.js

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

De esa manera, puede probar fácilmente helper1y helper2tal cual, sin usar Rewire y otras "magias" (que, según he descubierto, tienen sus propios puntos de dolor durante la depuración, o cuando intenta avanzar hacia TypeScript, sin mencionar que son más pobres) comprensibilidad para nuevos colegas). Y estar en una subcarpeta llamada internal, o algo así, ayudará a evitar su uso accidental en lugares no deseados.


PD: Otro problema común con los métodos de "privado" es que si quieres prueba publicMethod1y publicMethod2y burlas de los ayudantes, de nuevo, que normalmente necesita algo así como Recablear de hacer eso. Sin embargo, si están en archivos separados, puede usar Proxyquire para hacerlo, que, a diferencia de Rewire, no necesita ningún cambio en su proceso de compilación, es fácil de leer y depurar, y funciona bien incluso con TypeScript.

Dániel Kis-Nagy
fuente
1

Para hacer que los métodos privados estén disponibles para las pruebas, hago esto:

const _myPrivateMethod: () => {};

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

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

module.exports = methods;
MFB
fuente