Testowanie jednostkowe funkcji prywatnych za pomocą Mocha i Node.js.


137

Używam Mocha do testowania jednostkowego aplikacji napisanej dla Node.js.

Zastanawiam się, czy możliwe jest testowanie jednostkowe funkcji, które nie zostały wyeksportowane w module.

Przykład:

Mam wiele funkcji zdefiniowanych w ten sposób w foobar.js:

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

I kilka funkcji wyeksportowanych jako publiczne:

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

Przypadek testowy ma następującą strukturę:

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

Oczywiście to nie działa, ponieważ private_foobar1nie jest eksportowane.

Jaki jest prawidłowy sposób testowania prywatnych metod jednostkowych? Czy Mocha ma na to wbudowane metody?


Odpowiedzi:


68

Jeśli funkcja nie jest eksportowana przez moduł, nie można jej wywołać kodem testowym poza modułem. Wynika to z działania JavaScript, a Mocha sama nie może tego obejść.

W kilku przypadkach, w których stwierdziłem, że testowanie funkcji prywatnej jest właściwą rzeczą do zrobienia, ustawiłem pewną zmienną środowiskową, którą mój moduł sprawdza, aby określić, czy działa w konfiguracji testowej, czy nie. Jeśli działa w konfiguracji testowej, eksportuje dodatkowe funkcje, które mogę następnie wywołać podczas testowania.

Słowo „środowisko” jest tutaj używane luźno. Może to oznaczać sprawdzenie process.envlub coś innego, co może komunikować się z modułem „jesteś teraz testowany”. Przypadki , w których musiałem to zrobić, znajdowały się w środowisku RequireJS i użyłem module.configw tym celu.


3
Warunkowe eksportowanie wartości nie wydaje się być zgodne z modułami ES6. DostajęSyntaxError: 'import' and 'export' may only appear at the top level
aij

1
@aij tak, ze względu na statyczny eksport ES6, którego nie można użyć import, exportwewnątrz bloku. W końcu będziesz w stanie osiągnąć tego rodzaju rzeczy w ES6 za pomocą programu ładującego system. Jednym ze sposobów obejścia tego problemu jest teraz użycie module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')i przechowywanie różnic w kodzie es6 w odpowiednich plikach.
cchamberlain

2
Myślę, że jeśli masz pełne pokrycie, to testujesz wszystkie swoje funkcje prywatne, niezależnie od tego, czy je ujawniłeś, czy nie.
Ziggy,

1
@aij Możesz wyeksportować warunkowo ... zobacz tę odpowiedź: stackoverflow.com/questions/39583958/…
RayLoveless

188

Sprawdź moduł rewire . Umożliwia pobieranie (i manipulowanie) zmiennymi prywatnymi i funkcjami w module.

Więc w twoim przypadku użycie byłoby takie jak:

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 Większość mojego kodu jest w formie modułów AMD, których rewire nie jest w stanie obsłużyć (ponieważ moduły AMD są funkcjami, ale rewire nie obsługuje "zmiennych w ramach funkcji"). Lub jest transponowany, inny scenariusz, którego rewire nie może obsłużyć. W rzeczywistości ludzie, którzy zamierzają spojrzeć na rewire, dobrze by zrobili, gdyby najpierw przeczytali ograniczenia (z linkami wcześniej), zanim spróbują z niego skorzystać. Nie mam ani jednej aplikacji, która a) wymaga eksportowania „prywatnych” rzeczy ib) nie ma ograniczeń dotyczących ponownego połączenia.
Louis,

1
Tylko mały punkt, pokrycie kodu może nie odebrać testów napisanych w ten sposób. Przynajmniej to widziałem, używając wbudowanego narzędzia pokrycia Jest.
Mike Stead

Rewire również nie współgra dobrze z narzędziem do auto-mockowania żartu. Nadal szukam sposobu, aby wykorzystać zalety żartów i uzyskać dostęp do niektórych prywatnych varów.
btburton42

Więc próbowałem to zrobić, ale używam maszynopisu, który, jak sądzę, powoduje ten problem. Zasadniczo pojawia się następujący błąd: Cannot find module '../../package' from 'node.js'. Czy ktoś to zna?
Zapalony

ReWire działa dobrze w .ts, typescriptbiegnę za pomocą ts-node @clu
muthukumar Selvaraj

26

Oto naprawdę dobry przepływ pracy do przetestowania prywatnych metod, wyjaśniony przez Philipa Waltona, inżyniera Google na swoim blogu.

Zasada

  • Napisz swój kod normalnie
  • Powiąż swoje metody prywatne z obiektem w oddzielnym bloku kodu i oznacz go _(na przykład)
  • Otocz ten blok kodu komentarzami początku i końca

Następnie użyj zadania kompilacji lub własnego systemu kompilacji (na przykład kodu grunt-strip ), aby rozebrać ten blok do kompilacji produkcyjnych.

Twoje kompilacje testowe mają dostęp do prywatnego interfejsu API, a wersje produkcyjne nie.

Skrawek

Wpisz swój kod w ten sposób:

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

I twoje zadania Grunt, takie jak to:

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

Głębiej

W późniejszym artykule wyjaśnia „dlaczego” „testowanie metod prywatnych”


1
Znalazłem także wtyczkę webkit, która wygląda tak, jakby mogła obsługiwać podobny przepływ pracy: webpack-strip-block
JRulle

21

Jeśli wolisz, aby było to proste, po prostu wyeksportuj również prywatne elementy członkowskie, ale wyraźnie oddziel je od publicznego API pewną konwencją, np. Dodaj do nich prefiks _lub zagnieżdżaj je pod pojedynczym obiektem prywatnym .

var privateWorker = function() {
    return 1
}

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

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

7
Zrobiłem to w przypadkach, gdy cały moduł ma być naprawdę prywatny, a nie do powszechnego użytku. Ale w przypadku modułów ogólnego przeznaczenia wolę ujawniać to, czego potrzebuję do testowania, tylko wtedy, gdy testowany jest kod. Prawdą jest, że ostatecznie nie ma nic, co mogłoby uniemożliwić komuś dotarcie do prywatnych rzeczy przez sfałszowanie środowiska testowego, ale kiedy ktoś debuguje swoją własną aplikację, wolałbym, aby nie widzieli symboli, które nie muszą być część publicznego interfejsu API. W ten sposób nie ma natychmiastowej pokusy, aby nadużywać API do celów, do których nie jest przeznaczony.
Louis

2
możesz również użyć zagnieżdżonej składni {... private : {worker: worker}}
Jason

2
Jeśli moduł składa się wyłącznie z funkcji, nie widzę w tym żadnych wad. Jeśli utrzymujesz i mutujesz stan, uważaj ...
Ziggy,

5

Dodałem dodatkową funkcję, którą nazywam Internal () i stamtąd zwracam wszystkie funkcje prywatne. Ta funkcja Internal () jest następnie eksportowana. Przykład:

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

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

Możesz wywołać funkcje wewnętrzne w następujący sposób:

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

Najbardziej podoba mi się to rozwiązanie, ponieważ:

  • tylko jedna funkcja Internal () jest zawsze eksportowana. Ta funkcja Internal () jest zawsze używana do testowania funkcji prywatnych.
  • Jest łatwy do wdrożenia
  • Niski wpływ na kod produkcyjny (tylko jedna dodatkowa funkcja)

5

W tym celu stworzyłem pakiet npm, który może ci się przydać: require-from

Zasadniczo ujawniasz metody niepubliczne poprzez:

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

Uwaga: testExports może to być dowolna prawidłowa nazwa, z wyjątkiem exportsoczywiście.

I z innego modułu:

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

1
Nie widzę praktycznej korzyści z tej metody. Nie czyni to symboli „prywatnych” bardziej prywatnymi. (Każdy może zadzwonić requireFromz odpowiednimi parametrami.) Ponadto, jeśli moduł with textExportszostanie załadowany przez requirewywołanie przed jego requireFrom załadowaniem, requireFromzwróci undefined. (Właśnie to przetestowałem.) Chociaż często można kontrolować kolejność ładowania modułów, nie zawsze jest to praktyczne. (O czym świadczą niektóre pytania Mocha na SO.) To rozwiązanie również nie będzie działać z modułami typu AMD. (Codziennie ładuję moduły AMD w Node do testów.)
Louis

Nie powinno działać z modułami AMD! Node.js używa common.js i jeśli zmienisz go na AMD, robisz to poza normą.
jemiloii

@JemiloII Setki programistów używają Node.js codziennie do testowania modułów AMD. Nie ma w tym nic „poza normą”. Można tylko powiedzieć, że Node.js nie jest wyposażony w moduł ładujący AMD, ale to nie mówi wiele, ponieważ Node zapewnia wyraźne punkty zaczepienia, aby rozszerzyć program ładujący, aby ładował dowolny format, który programiści chcą opracować.
Louis

To jest poza normą. Jeśli musisz ręcznie dołączyć program ładujący amd, nie jest to normą dla node.js. Rzadko widzę AMD dla kodu node.js. Zobaczę to dla przeglądarki, ale node. Nie. Nie mówię, że to się nie robi, tylko pytanie i odpowiedź, którą komentujemy, nie mówią nic o modułach AMD. Więc jeśli nikt nie twierdzi, że używa modułu ładującego amd, eksport węzłów nie powinien działać z amd. Chociaż chcę zauważyć, commonjs może być na dobrej drodze dzięki eksportowi es6. Mam tylko nadzieję, że pewnego dnia wszyscy będziemy mogli użyć tylko jednej metody eksportu.
jemiloii

3

Wiem, że niekoniecznie jest to odpowiedź, której szukasz, ale stwierdziłem, że w większości przypadków, jeśli funkcja prywatna jest warta przetestowania, warto być w jej własnym pliku.

Np. Zamiast mieć prywatne metody w tym samym pliku co publiczne, na przykład ...

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

... podzielisz to w ten sposób:

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

W ten sposób możesz łatwo testować helper1i helper2tak jak jest, bez używania Rewire i innych "magicznych" (które, jak odkryłem, mają swoje własne problemy podczas debugowania lub gdy próbujesz przejść w kierunku TypeScript, nie wspominając o gorszej zrozumiałość dla nowych kolegów). A umieszczenie ich w podfolderze o nazwie internallub czymś podobnym pomoże uniknąć przypadkowego użycia ich w niezamierzonych miejscach.


PS: Innym częstym problemem związanym z metodami „prywatnymi” jest to, że jeśli chcesz przetestować publicMethod1i publicMethod2wyśmiać pomocników, zwykle potrzebujesz do tego czegoś takiego jak Rewire. Jeśli jednak znajdują się w osobnych plikach, możesz to zrobić za pomocą Proxyquire , który w przeciwieństwie do Rewire nie wymaga żadnych zmian w procesie kompilacji, jest łatwy do odczytania i debugowania oraz działa dobrze nawet z TypeScript.


2

Poszedłem za odpowiedzią Barwina i sprawdziłem, jak można wykonać testy jednostkowe za pomocą modułu rewire . Mogę potwierdzić, że to rozwiązanie po prostu działa.

Moduł powinien składać się z dwóch części - publicznej i prywatnej. W przypadku funkcji publicznych możesz to zrobić w standardowy sposób:

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

W zakresie prywatnym:

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

Aby dowiedzieć się więcej na ten temat, stworzyłem działający przykład z pełnym testowaniem modułów, testowanie obejmuje zakres prywatny i publiczny.

W celu uzyskania dalszych informacji zachęcam do zapoznania się z artykułem ( Jak przetestować prywatne funkcje modułu CommonJS ) w pełni opisującym temat. Zawiera przykłady kodu.


1

Aby udostępnić prywatne metody do testowania, robię to:

const _myPrivateMethod: () => {};

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

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

module.exports = methods;

Wytłumaczenie byłoby w porządku. Na przykład, w jaki sposób iw jakim kontekście testustawiana jest zmienna środowiskowa ?
Peter Mortensen
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.