Unit-Test von privaten Funktionen mit mocha und node.js.


131

Ich verwende Mokka, um eine für node.js geschriebene Anwendung zu testen

Ich frage mich, ob es möglich ist, Unit-Test-Funktionen durchzuführen, die nicht in ein Modul exportiert wurden.

Beispiel:

Ich habe viele Funktionen wie diese in definiert foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

und einige Funktionen, die als öffentlich exportiert wurden:

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

Der Testfall ist wie folgt aufgebaut:

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

Offensichtlich funktioniert dies nicht, da private_foobar1es nicht exportiert wird.

Was ist der richtige Weg, um private Methoden zu testen? Hat Mokka einige eingebaute Methoden, um das zu tun?


Antworten:


64

Wenn die Funktion nicht vom Modul exportiert wird, kann sie nicht durch Testcode außerhalb des Moduls aufgerufen werden. Das liegt daran, wie JavaScript funktioniert, und Mocha kann dies nicht alleine umgehen.

In den wenigen Fällen, in denen ich festgestellt habe, dass das Testen einer privaten Funktion das Richtige ist, habe ich eine Umgebungsvariable festgelegt, die mein Modul überprüft, um festzustellen, ob sie in einem Test-Setup ausgeführt wird oder nicht. Wenn es im Test-Setup ausgeführt wird, exportiert es zusätzliche Funktionen, die ich dann während des Tests aufrufen kann.

Das Wort "Umwelt" wird hier lose verwendet. Dies kann bedeuten, process.envdass das Modul "Sie werden gerade getestet" überprüft wird oder etwas anderes. Die Fälle, in denen ich dies tun musste, waren in einer RequireJS-Umgebung und ich habe sie module.configfür diesen Zweck verwendet.


2
Das bedingte Exportieren von Werten scheint nicht mit ES6-Modulen kompatibel zu sein. Ich bekommeSyntaxError: 'import' and 'export' may only appear at the top level
aij

1
@aij ja aufgrund ES6 statischen Exporte können Sie nicht verwenden import, exportinnerhalb eines Blocks. Schließlich können Sie dies in ES6 mit dem Systemloader erreichen. Eine Möglichkeit, dies jetzt zu module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')umgehen, besteht darin , Ihre es6-Codeunterschiede in den jeweiligen Dateien zu verwenden und zu speichern.
Cchamberlain

2
Ich denke, wenn Sie eine vollständige Abdeckung haben, testen Sie alle Ihre privaten Funktionen, unabhängig davon, ob Sie sie verfügbar gemacht haben oder nicht.
Ziggy

1
@aij Sie können unter bestimmten Bedingungen exportieren ... siehe diese Antwort: stackoverflow.com/questions/39583958/…
RayLoveless

187

Überprüfen Sie die Neuverdrahtung . Sie können damit private Variablen und Funktionen innerhalb eines Moduls abrufen (und bearbeiten).

In Ihrem Fall wäre die Verwendung also ungefähr so:

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 Der größte Teil meines Codes besteht entweder aus AMD-Modulen, die von rewire nicht verarbeitet werden können (da AMD-Module Funktionen sind, rewire jedoch keine "Variablen innerhalb von Funktionen" verarbeiten kann). Oder ist transpiliert, ein anderes Szenario, das Rewire nicht bewältigen kann. Tatsächlich sollten Personen, die sich mit Rewire befassen, zuerst die (zuvor verlinkten) Einschränkungen lesen, bevor sie versuchen, sie zu verwenden. Ich habe keine einzige App, die a) "private" Inhalte exportieren muss und b) nicht auf eine Einschränkung der Neuverdrahtung stößt.
Louis

1
Nur ein kleiner Punkt: Die Codeabdeckung kann solche Tests möglicherweise nicht erfassen. Zumindest habe ich das mit Jests integriertem Coverage-Tool gesehen.
Mike Stead

Rewire spielt auch nicht gut mit dem Auto-Mocking-Tool von jest. Ich bin immer noch auf der Suche nach einer Möglichkeit, die Vorteile von Jest zu nutzen und auf einige private Vars zuzugreifen.
BTburton42

Also habe ich versucht, dies zum Laufen zu bringen, aber ich verwende Typoskript, was vermutlich dieses Problem verursacht. Grundsätzlich erhalte ich folgenden Fehler : Cannot find module '../../package' from 'node.js'. Kennt jemand das?
Clu

rewire funktioniert gut in .ts, typescriptich laufe mit ts-node @clu
muthukumar selvaraj

24

Hier ist ein wirklich guter Workflow zum Testen Ihrer privaten Methoden , der von Philip Walton, einem Google-Ingenieur in seinem Blog, erklärt wurde.

Prinzip

  • Schreiben Sie Ihren Code normal
  • Binden Sie Ihre privaten Methoden in einem separaten Codeblock an das Objekt und markieren Sie es _beispielsweise mit einem
  • Umgeben Sie diesen Codeblock mit Start- und Endkommentaren

Verwenden Sie dann eine Build-Task oder Ihr eigenes Build-System (z. B. Grunt-Strip-Code), um diesen Block für Produktions-Builds zu entfernen.

Ihre Test-Builds haben Zugriff auf Ihre private API und Ihre Produktions-Builds nicht.

Snippet

Schreiben Sie Ihren Code wie folgt:

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

Und deine Grunzaufgaben so

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

Tiefer

In einem späteren Artikel wird das "Warum" des "Testens privater Methoden" erläutert.


1
Es wurde auch ein Webkit-Plugin gefunden, das einen ähnlichen Workflow unterstützen kann: webpack-strip-block
JRulle

21

Wenn Sie es lieber einfach halten möchten, exportieren Sie einfach auch die privaten Mitglieder, die jedoch durch eine Konvention klar von der öffentlichen API getrennt sind, z. B. indem Sie ihnen ein Präfix voranstellen _oder sie unter einem einzelnen privaten Objekt verschachteln .

var privateWorker = function() {
    return 1
}

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

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

7
Ich habe dies in Fällen getan, in denen das gesamte Modul wirklich privat und nicht für den allgemeinen Gebrauch bestimmt ist. Für Allzweckmodule ziehe ich es jedoch vor, das, was ich zum Testen benötige, nur dann offenzulegen , wenn der Code getestet wird. Es ist wahr, dass es letztendlich nichts gibt, das jemanden daran hindert, durch Fälschung einer Testumgebung zu den privaten Dingen zu gelangen, aber wenn man seine eigene Anwendung debuggt, möchte ich lieber nicht die Symbole sehen, die nicht sein müssen Teil der öffentlichen API. Auf diese Weise besteht keine unmittelbare Versuchung, die API für Zwecke zu missbrauchen, für die sie nicht entwickelt wurde.
Louis

2
Sie können auch verschachtelte Syntax verwenden {... privat : {Arbeiter: Arbeiter}}
Jason

2
Wenn das Modul nur aus reinen Funktionen besteht, sehe ich keinen Nachteil darin. Wenn Sie den Zustand beibehalten und mutieren, dann seien Sie vorsichtig ...
Ziggy

5

Zu diesem Zweck habe ich ein npm-Paket erstellt, das Sie möglicherweise nützlich finden: require-from

Grundsätzlich legen Sie nicht öffentliche Methoden offen, indem Sie:

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

Hinweis: testExports kann ein beliebiger gültiger Name sein, außer exportsnatürlich.

Und aus einem anderen Modul:

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

1
Ich sehe keinen praktischen Vorteil für diese Methode. Die "privaten" Symbole werden dadurch nicht privater. (Jeder kann requireFrommit den richtigen Parametern aufrufen .) Wenn das Modul mit textExportsdurch einen requireAufruf geladen wird , bevor requireFrom es geladen wird , requireFromwird es zurückgegeben undefined. (Ich habe es gerade getestet.) Obwohl es oft möglich ist, die Ladereihenfolge von Modulen zu steuern, ist dies nicht immer praktisch. (Wie einige Mokka-Fragen zu SO belegen.) Diese Lösung funktioniert im Allgemeinen auch nicht mit AMD-Modulen. (Ich lade AMD-Module täglich in Node zum Testen.)
Louis

Es sollte nicht mit AMD-Modulen funktionieren! Node.js verwendet common.js. Wenn Sie es ändern, um AMD zu verwenden, tun Sie dies außerhalb der Norm.
jemiloii

@JemiloII Hunderte von Entwicklern verwenden Node.js täglich, um AMD-Module zu testen. Das ist nichts "Außergewöhnliches". Das Beste, was Sie sagen können, ist, dass Node.js nicht mit einem AMD-Loader geliefert wird, aber dies sagt nicht viel aus, da Node explizite Hooks bereitstellt, um seinen Loader zu erweitern und das Format zu laden, das Entwickler entwickeln möchten.
Louis

Es ist nicht normal. Wenn Sie einen AMD-Loader manuell einbinden müssen, ist dies nicht die Norm für node.js. Ich sehe AMD selten für den Code von node.js. Ich werde es für den Browser sehen, aber Knoten. Ich sage nicht, dass es nicht gemacht wird, nur die Frage und diese Antwort, die wir kommentieren, sagen nichts über AMD-Module aus. Ohne dass jemand angibt, dass er einen AMD-Loader verwendet, sollten Knotenexporte nicht mit AMD funktionieren. Obwohl ich erwähnen möchte, könnten CommonJs mit den es6-Exporten auf dem Weg nach draußen sein. Ich hoffe nur, dass wir eines Tages alle nur eine Exportmethode verwenden können.
jemiloii

4

Ich habe eine zusätzliche Funktion hinzugefügt, die ich Internal () nenne und von dort aus alle privaten Funktionen zurückgebe. Diese Internal () -Funktion wird dann exportiert. Beispiel:

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

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

Sie können die internen Funktionen folgendermaßen aufrufen:

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

Diese Lösung gefällt mir am besten, weil:

  • Es wird immer nur eine Funktion Internal () exportiert. Diese interne () -Funktion wird immer zum Testen privater Funktionen verwendet.
  • Es ist einfach zu implementieren
  • Geringe Auswirkung auf den Produktionscode (nur eine zusätzliche Funktion)

2

Ich folgte der Antwort von @barwin und überprüfte, wie Unit-Tests mit Rewire durchgeführt werden können Modul durchgeführt werden können. Ich kann bestätigen, dass diese Lösung einfach funktioniert.

Das Modul sollte aus zwei Teilen bestehen - dem öffentlichen und dem privaten. Für öffentliche Funktionen können Sie dies auf standardmäßige Weise tun:

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

Für den privaten Bereich:

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

Um mehr über das Thema zu erfahren, habe ich ein Arbeitsbeispiel mit vollständigen Modultests erstellt. Das Testen umfasst den privaten und öffentlichen Bereich.

Für weitere Informationen empfehle ich Ihnen, den Artikel ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ) zu lesen, der das Thema vollständig beschreibt und Codebeispiele enthält.


2

Ich weiß, dass dies nicht unbedingt die Antwort ist, nach der Sie suchen, aber ich habe festgestellt, dass es sich die meiste Zeit, wenn eine private Funktion getestet werden sollte, lohnt, in einer eigenen Datei zu sein.

ZB anstatt private Methoden in derselben Datei wie die öffentlichen zu haben, wie diese ...

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

... du hast es so aufgeteilt:

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

Auf diese Weise können Sie problemlos testen helper1und helper2unverändert, ohne Rewire und andere "Magie" zu verwenden (die, wie ich festgestellt habe, ihre eigenen Schwachstellen beim Debuggen haben oder wenn Sie versuchen, auf TypeScript umzusteigen, ganz zu schweigen von schlechteren Verständlichkeit für neue Kollegen). Und wenn sie sich in einem Unterordner namens internaloder so ähnlich befinden, können Sie verhindern, dass sie versehentlich an unbeabsichtigten Orten verwendet werden.


PS: Ein weiteres häufiges Problem mit „privaten“ Methoden ist , dass , wenn Sie testen mögen , publicMethod1und publicMethod2und die Helfer verspotten, wieder, Sie in der Regel so etwas wie Rewire brauchen , das zu tun. Wenn sie sich jedoch in separaten Dateien befinden, können Sie Proxyquire verwenden, was im Gegensatz zu Rewire keine Änderungen an Ihrem Erstellungsprozess erfordert, einfach zu lesen und zu debuggen ist und auch mit TypeScript gut funktioniert.


1

Um private Methoden zum Testen verfügbar zu machen, mache ich Folgendes:

const _myPrivateMethod: () => {};

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

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

module.exports = methods;
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.