unit test di funzioni private con mocha e node.js


131

Sto usando moka per testare un'unità un'applicazione scritta per node.js

Mi chiedo se sia possibile eseguire funzioni di unit test che non sono state esportate in un modulo.

Esempio:

Ho molte funzioni definite in questo modo in foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

e alcune funzioni esportate come pubbliche:

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

Il caso di test è strutturato come segue:

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

Ovviamente questo non funziona, poiché private_foobar1non viene esportato.

Qual è il modo corretto di testare metodi privati? La moka ha alcuni metodi integrati per farlo?


Risposte:


64

Se la funzione non viene esportata dal modulo, non può essere richiamata dal codice di test esterno al modulo. Ciò è dovuto al modo in cui funziona JavaScript e Mocha non può da sola aggirare questo.

Nei pochi casi in cui ho determinato che testare una funzione privata è la cosa giusta da fare, quello che ho fatto è impostare una variabile d'ambiente che il mio modulo controlla per determinare se è in esecuzione in una configurazione di prova o meno. Se viene eseguito nella configurazione del test, esporta funzioni aggiuntive che posso quindi chiamare durante il test.

La parola "ambiente" è qui usata vagamente. Potrebbe significare il controllo process.envo qualcos'altro che può comunicare al modulo "sei in fase di test ora". I casi in cui ho dovuto farlo erano in un ambiente RequireJS, e ho usato module.configper questo scopo.


2
L'esportazione condizionale dei valori non sembra essere compatibile con i moduli ES6. RicevoSyntaxError: 'import' and 'export' may only appear at the top level
aij

1
@aij sì a causa delle esportazioni statiche ES6 che non puoi usare import, exportall'interno di un blocco. Alla fine sarai in grado di compiere questo genere di cose in ES6 con il Caricatore di sistema. Un modo per aggirare il problema ora è utilizzare module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')e archiviare le differenze del codice es6 nei rispettivi file.
cchamberlain,

2
Immagino che se hai una copertura completa, stai testando tutte le tue funzioni private, sia che tu le abbia esposte o meno.
Ziggy,

1
@aij È possibile esportare condizionale ... vedere questa risposta: stackoverflow.com/questions/39583958/...
RayLoveless

187

Dai un'occhiata al modulo rewire . Ti permette di ottenere (e manipolare) variabili e funzioni private all'interno di un modulo.

Quindi nel tuo caso l'utilizzo sarebbe qualcosa del tipo:

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).....

3
@Jaro La maggior parte del mio codice è sotto forma di moduli AMD, che il rewire non è in grado di gestire (poiché i moduli AMD sono funzioni ma il rewire non può gestire "variabili all'interno di funzioni"). O viene traspilato, un altro scenario che rewire non può gestire. In realtà, le persone che guarderanno il ricablaggio farebbero bene a leggere i limiti (collegati in precedenza) prima di provare a usarlo. Non ho una singola app che a) abbia bisogno di esportare roba "privata" eb) non abbia un limite di ricablaggio.
Louis,

1
Solo un piccolo punto, la copertura del codice potrebbe non riuscire a raccogliere i test scritti in questo modo. Almeno questo è quello che ho visto usando lo strumento di copertura integrato di Jest.
Mike Stead,

Anche Rewire non gioca bene con lo strumento auto-beffardo di jest. Sto ancora cercando un modo per sfruttare i benefici di jest e accedere ad alcuni privati.
btburton42,

Quindi ho provato a farlo funzionare ma sto usando dattiloscritto, che suppongo stia causando questo problema. Fondamentalmente io ottengo il seguente errore: Cannot find module '../../package' from 'node.js'. Qualcuno ha familiarità con questo?
clu,

rewire funziona perfettamente .ts, typescriptcorro usando ts-node @clu
muthukumar selvaraj

24

Ecco un ottimo flusso di lavoro per testare i tuoi metodi privati spiegati da Philip Walton, un ingegnere di Google sul suo blog.

Principio

  • Scrivi il tuo codice normalmente
  • Associa i tuoi metodi privati ​​all'oggetto in un blocco di codice separato, contrassegnalo con un _esempio
  • Circonda quel blocco di codice dai commenti di inizio e fine

Quindi utilizzare un'attività di compilazione o il proprio sistema di compilazione (ad esempio grunt-strip-code) per rimuovere questo blocco per build di produzione.

Le build di test hanno accesso alle API private e le build di produzione no.

Frammento

Scrivi il tuo codice come questo:

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 i tuoi compiti grugniti in quel modo

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

Più in profondità

In un articolo successivo , spiega il "perché" del "test di metodi privati"


1
Ho anche trovato un plug-in webkit che sembra supportare un flusso di lavoro simile: webpack-strip-block
JRulle

21

Se preferisci mantenerlo semplice, esporta anche i membri privati, ma chiaramente separati dall'API pubblica con alcune convenzioni, ad esempio prefissandoli con _o annidali sotto un singolo oggetto privato .

var privateWorker = function() {
    return 1
}

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

module.exports = {
    doSomething: doSomething,
    _privateWorker: privateWorker
}

7
L'ho fatto nei casi in cui l'intero modulo è davvero pensato per essere privato e non per consumo comune. Ma per i moduli generici preferisco esporre ciò di cui ho bisogno per il test solo quando il codice viene testato. È vero che alla fine non c'è nulla che possa impedire a qualcuno di accedere alle cose private falsificando un ambiente di test ma quando si esegue il debug sulla propria applicazione, preferirei che non vedessero i simboli che non devono essere parte dell'API pubblica. In questo modo non c'è la tentazione immediata di abusare dell'API per scopi per cui non è progettato.
Louis,

2
puoi anche usare la sintassi nidificata {... private : {worker: worker}}
Jason

2
Se il modulo ha tutte le funzioni pure, non vedo alcun aspetto negativo nel fare questo. Se stai mantenendo e mutando lo stato, fai attenzione ...
Ziggy,

5

Ho creato un pacchetto npm con questo scopo che potresti trovare utile: request -from

Fondamentalmente si espongono metodi non pubblici mediante:

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

nota: testExports può essere qualsiasi nome valido che desideri, tranne exportsovviamente.

E da un altro modulo:

var requireFrom = require('require-from');
var private_foobar1 = requireFrom('testExports', './path-to-module').private_foobar1;

1
Non vedo alcun vantaggio pratico a questo metodo. Non rende i simboli "privati" più privati. (Chiunque può chiamare requireFromcon i parametri giusti.) Inoltre, se il modulo con textExportsviene caricato da una requirechiamata prima di requireFrom caricarlo, requireFromtornerà undefined. (L'ho appena provato.) Sebbene sia spesso possibile controllare l'ordine di caricamento dei moduli, non è sempre pratico. (Come evidenziato da alcune domande di Mocha su SO.) Questa soluzione non funzionerà generalmente con moduli di tipo AMD. (Carico i moduli AMD in Node su base giornaliera per i test.)
Louis,

Non dovrebbe funzionare con i moduli AMD! Node.js usa common.js e se lo cambi per usare AMD, lo stai facendo fuori dalla norma.
jemiloii,

@JemiloII Centinaia di sviluppatori usano Node.js quotidianamente per testare i moduli AMD. Non c'è niente "fuori dalla norma" nel farlo. Il massimo che puoi dire è che Node.js non viene fornito con un caricatore AMD, ma questo non sta dicendo molto, visto che Node fornisce hook espliciti per estendere il suo caricatore per caricare qualunque formato gli sviluppatori vogliano sviluppare.
Louis,

È fuori dalla norma. Se devi includere manualmente un caricatore amd, non è la norma per node.js. Raramente vedo AMD per il codice node.js. Lo vedrò per il browser, ma nodo. No. Non sto dicendo che non sia stato fatto, solo la domanda e questa risposta che stiamo commentando, non dicono nulla sui moduli AMD. Quindi, senza che nessuno affermi di utilizzare un caricatore AMD, le esportazioni di nodi non dovrebbero funzionare con AMD. Anche se voglio notare, i commonjs potrebbero uscire con le esportazioni es6. Spero solo che un giorno tutti possiamo usare un solo metodo di esportazione.
jemiloii,

4

Ho aggiunto una funzione extra che ho chiamato Internal () e restituisco tutte le funzioni private da lì. Questa funzione interna () viene quindi esportata. Esempio:

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

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

Puoi chiamare le funzioni interne in questo modo:

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

Mi piace di più questa soluzione perché:

  • viene sempre esportata solo una funzione Internal () . Questa funzione Internal () viene sempre utilizzata per testare le funzioni private.
  • È semplice da implementare
  • Basso impatto sul codice di produzione (solo una funzione aggiuntiva)

2

Ho seguito la risposta di @barwin e verificato come è possibile effettuare test unitari con il modulo rewire . Posso confermare che questa soluzione funziona semplicemente.

Il modulo dovrebbe essere richiesto in due parti: una pubblica e una privata. Per le funzioni pubbliche puoi farlo in modo standard:

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

Per ambito privato:

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

Per saperne di più sull'argomento, ho creato un esempio funzionante con test del modulo completo, il test include ambito privato e pubblico.

Per ulteriori informazioni, ti incoraggio a consultare l'articolo ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ) che descrive completamente l'argomento, include esempi di codice.


2

So che questa non è necessariamente la risposta che stai cercando, ma quello che ho trovato è che la maggior parte delle volte se vale la pena testare una funzione privata, vale la pena trovarsi nel suo file.

Ad esempio invece di avere metodi privati ​​nello stesso file di quelli pubblici, come questo ...

src / cosa / 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 hai suddiviso in questo modo:

src / cosa / 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 / cosa / interni / helper1.js

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

src / cosa / interni / helper2.js

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

In questo modo, puoi facilmente testare helper1e così helper2com'è, senza usare Rewire e altre "magie" (che, ho scoperto, hanno i loro punti deboli durante il debug o quando provi a muoverti verso TypeScript, per non parlare dei più poveri comprensibilità per i nuovi colleghi). E il fatto che si trovino in una sottocartella denominata internal, o qualcosa del genere, aiuterà a evitare un uso accidentale di essi in luoghi non previsti.


PS: Un altro problema comune con i metodi "privati" è che se si vuole testare publicMethod1e publicMethod2e deridere l'aiutanti, ancora una volta, che normalmente bisogno di qualcosa come Rewire per farlo. Tuttavia, se si trovano in file separati, è possibile utilizzare Proxyquire per farlo, che, a differenza di Rewire, non ha bisogno di modifiche al processo di compilazione, è facile da leggere e da debug e funziona bene anche con TypeScript.


1

Per rendere disponibili metodi privati ​​per i test, faccio questo:

const _myPrivateMethod: () => {};

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

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

module.exports = methods;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.