test unitaire de fonctions privées avec mocha et node.js


131

J'utilise mocha afin de tester unitaire une application écrite pour node.js

Je me demande s'il est possible de tester des fonctions unitaires qui n'ont pas été exportées dans un module.

Exemple:

J'ai beaucoup de fonctions définies comme ça dans foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

et quelques fonctions exportées en public:

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

Le scénario de test est structuré comme suit:

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

Évidemment, cela ne fonctionne pas, car il private_foobar1n'est pas exporté.

Quelle est la bonne façon de tester les méthodes privées unitaire? Mocha a-t-il des méthodes intégrées pour faire cela?


Réponses:


64

Si la fonction n'est pas exportée par le module, elle ne peut pas être appelée par code de test en dehors du module. Cela est dû au fonctionnement de JavaScript, et Mocha ne peut pas à lui seul contourner cela.

Dans les quelques cas où j'ai déterminé que tester une fonction privée était la bonne chose à faire, j'ai défini une variable d'environnement que mon module vérifie pour déterminer si elle s'exécute dans une configuration de test ou non. S'il s'exécute dans la configuration de test, il exporte des fonctions supplémentaires que je peux ensuite appeler pendant le test.

Le mot «environnement» est utilisé ici de manière vague. Cela peut signifier une vérification process.envou autre chose qui peut communiquer au module "vous êtes testé maintenant". Les instances où j'ai dû faire cela se trouvaient dans un environnement RequireJS, et j'ai utilisé module.configà cette fin.


2
L'exportation conditionnelle des valeurs ne semble pas compatible avec les modules ES6. Je reçoisSyntaxError: 'import' and 'export' may only appear at the top level
aij

1
@aij oui en raison d'exportations statiques ES6 que vous ne pouvez pas utiliser import, à l' exportintérieur d'un bloc. Finalement, vous serez en mesure d'accomplir ce genre de chose dans ES6 avec le chargeur système. Une façon de le contourner maintenant consiste à utiliser module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')et à stocker vos différences de code es6 dans ces fichiers respectifs.
cchamberlain

2
Je suppose que si vous avez une couverture complète, vous testez toutes vos fonctions privées, que vous les ayez exposées ou non.
Ziggy

1
@aij Vous pouvez exporter conditionnellement ... voir cette réponse: stackoverflow.com/questions/39583958/…
RayLoveless

187

Découvrez le module de recâblage . Il vous permet d'obtenir (et de manipuler) des variables et des fonctions privées dans un module.

Donc, dans votre cas, l'utilisation serait quelque chose comme:

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 plupart de mon code est soit sous la forme de modules AMD, que rewire est incapable de gérer (parce que les modules AMD sont des fonctions mais que rewire ne peut pas gérer les «variables dans les fonctions»). Ou est transpiled, un autre scénario que rewire ne peut pas gérer. En fait, les personnes qui vont se pencher sur le rewire feraient bien de lire d'abord les limitations (liées plus tôt) avant d'essayer de l'utiliser. Je n'ai pas une seule application qui a) a besoin d'exporter des trucs "privés" et b) ne se heurte pas à une limitation de rewire.
Louis

1
Juste un petit point, la couverture du code peut échouer à ramasser les tests écrits comme ça. Du moins, c'est ce que j'ai vu en utilisant l'outil de couverture intégré de Jest.
Mike Stead

Rewire ne joue pas non plus bien avec l'outil de simulation automatique de jest. Je suis toujours à la recherche d'un moyen de tirer parti des avantages de Jest et d'accéder à des vars privés.
btburton42

J'ai donc essayé de faire ce travail, mais j'utilise du dactylographié, ce qui, je suppose, est à l'origine de ce problème. Fondamentalement , je reçois l'erreur suivante: Cannot find module '../../package' from 'node.js'. Quelqu'un connaît-il cela?
clu

rewire fonctionne bien dans .ts, typescriptje cours en utilisant ts-node @clu
muthukumar selvaraj

24

Voici un très bon workflow pour tester vos méthodes privées expliqué par Philip Walton, un ingénieur Google sur son blog.

Principe

  • Écrivez votre code normalement
  • Liez vos méthodes privées à l'objet dans un bloc de code séparé, marquez-le par un _par exemple
  • Entourez ce bloc de code de commentaires de début et de fin

Ensuite, utilisez une tâche de construction ou votre propre système de construction (par exemple grunt-strip-code) pour supprimer ce bloc pour les versions de production.

Vos versions de test ont accès à votre API privée, contrairement à vos versions de production.

Fragment

Écrivez votre code comme ceci:

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

Et tes tâches grognantes comme ça

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

Plus profond

Dans un article ultérieur , il explique le «pourquoi» du «test de méthodes privées»



21

Si vous préférez garder les choses simples, exportez simplement les membres privés également, mais clairement séparés de l'API publique avec une convention, par exemple les préfixer avec un _ou les imbriquer sous un seul objet privé .

var privateWorker = function() {
    return 1
}

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

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

7
J'ai fait cela dans les cas où le module entier est vraiment destiné à être privé et non à une consommation courante. Mais pour les modules à usage général, je préfère exposer ce dont j'ai besoin pour les tests uniquement lorsque le code est testé. Il est vrai qu'en fin de compte, rien n'empêcherait quelqu'un d'accéder aux choses privées en simulant un environnement de test, mais quand on fait le débogage sur sa propre application, je préfère ne pas voir les symboles qui n'ont pas besoin d'être partie de l'API publique. De cette façon, il n'y a pas de tentation immédiate d'abuser de l'API à des fins pour lesquelles elle n'est pas conçue.
Louis

2
vous pouvez également utiliser la syntaxe imbriquée {... private : {worker: worker}}
Jason

2
Si le module est composé uniquement de fonctions pures, je ne vois aucun inconvénient à le faire. Si vous maintenez et mutez l'état, alors méfiez-vous ...
Ziggy

5

J'ai créé un package npm à cet effet que vous pourriez trouver utile: require-from

Fondamentalement, vous exposez des méthodes non publiques en:

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

Remarque: testExports peut être n'importe quel nom valide que vous voulez, sauf exportsbien sûr.

Et depuis un autre module:

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

1
Je ne vois aucun avantage pratique à cette méthode. Cela ne rend pas les symboles «privés» plus privés. (N'importe qui peut appeler requireFromavec les bons paramètres.) Aussi, si le module avectextExports est chargé par un requireappel avant de le requireFrom charger, requireFromil retournera undefined. (Je viens de le tester.) Bien qu'il soit souvent possible de contrôler l'ordre de chargement des modules, ce n'est pas toujours pratique. (Comme en témoignent certaines questions de Mocha sur SO.) Cette solution ne fonctionnera généralement pas non plus avec les modules de type AMD. (Je charge des modules AMD dans Node quotidiennement pour les tests.)
Louis

Cela ne devrait pas fonctionner avec les modules AMD! Node.js utilise common.js et si vous le changez pour utiliser AMD, alors vous le faites hors de la norme.
jemiloii

@JemiloII Des centaines de développeurs utilisent Node.js quotidiennement pour tester les modules AMD. Il n'y a rien de «hors norme» à faire cela. Tout ce que vous pouvez dire, c'est que Node.js n'est pas livré avec un chargeur AMD, mais cela ne dit pas grand-chose, car Node fournit des hooks explicites pour étendre son chargeur afin de charger le format que les développeurs souhaitent développer.
Louis

C'est hors norme. Si vous devez inclure manuellement un chargeur amd, ce n'est pas la norme pour node.js. Je vois rarement AMD pour le code node.js. Je vais le voir pour le navigateur, mais node. Non, je ne dis pas que ce n'est pas fait, juste la question et cette réponse que nous commentons, ne dites rien sur les modules amd. Donc, sans que personne n'indique qu'il utilise un chargeur amd, les exportations de nœuds ne devraient pas fonctionner avec amd. Bien que je veuille noter, commonjs pourrait être en voie de disparition avec les exportations es6. J'espère juste qu'un jour nous pourrons tous utiliser une seule méthode d'exportation.
jemiloii

4

J'ai ajouté une fonction supplémentaire que je nomme Internal () et renvoie toutes les fonctions privées à partir de là. Cette fonction interne () est ensuite exportée. Exemple:

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

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

Vous pouvez appeler les fonctions internes comme ceci:

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

J'aime mieux cette solution parce que:

  • une seule fonction interne () est toujours exportée. Cette fonction Internal () est toujours utilisée pour tester les fonctions privées.
  • C'est simple à mettre en œuvre
  • Faible impact sur le code de production (une seule fonction supplémentaire)

2

J'ai suivi la réponse de @barwin et vérifié comment les tests unitaires peuvent être effectués avec rewire module . Je peux confirmer que cette solution fonctionne simplement.

Le module devrait être exigé en deux parties - une publique et une privée. Pour les fonctions publiques, vous pouvez le faire de manière standard:

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

Pour une portée privée:

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

Afin d'en savoir plus sur le sujet, j'ai créé un exemple de travail avec des tests de modules complets, les tests incluent une portée privée et publique.

Pour plus d'informations, je vous encourage à consulter l'article ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ) décrivant complètement le sujet, il comprend des exemples de code.


2

Je sais que ce n'est pas nécessairement la réponse que vous recherchez, mais ce que j'ai trouvé, c'est que la plupart du temps, si une fonction privée vaut la peine d'être testée, elle vaut la peine d'être dans son propre fichier.

Par exemple, au lieu d'avoir des méthodes privées dans le même fichier que les méthodes publiques, comme ceci ...

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

... vous le divisez comme ceci:

src / chose / 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 / chose / interne / helper1.js

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

src / chose / interne / helper2.js

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

De cette façon, vous pouvez facilement tester helper1et helper2tel quel, sans utiliser Rewire et d'autres "magies" (qui, j'ai trouvé, ont leurs propres points faibles lors du débogage, ou lorsque vous essayez de vous déplacer vers TypeScript, pour ne pas mentionner plus pauvre compréhensibilité pour les nouveaux collègues). Et le fait de les placer dans un sous-dossier appelé internal, ou quelque chose du genre, aidera à éviter leur utilisation accidentelle dans des endroits non souhaités.


PS: Une autre question commune avec des méthodes « privées » est que si vous voulez tester publicMethod1et publicMethod2et se moquer des aides, encore une fois, vous avez normalement besoin de quelque chose comme ReWire de le faire. Cependant, s'ils sont dans des fichiers séparés, vous pouvez utiliser Proxyquire pour le faire, qui, contrairement à Rewire, ne nécessite aucune modification de votre processus de construction, est facile à lire et à déboguer, et fonctionne bien même avec TypeScript.


1

Pour rendre les méthodes privées disponibles pour les tests, je fais ceci:

const _myPrivateMethod: () => {};

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

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

module.exports = methods;
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.