Jak przeprowadzić test jednostkowy modułu Node.js, który wymaga innych modułów i jak mockować globalną funkcję wymagania?


156

Oto trywialny przykład, który ilustruje sedno mojego problemu:

var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.doComplexStuff();
}

module.exports = underTest;

Próbuję napisać test jednostkowy dla tego kodu. Jak mogę wyszydzić wymaganie dla innerLibbez requirecałkowitego wyszydzania funkcji?

Więc to ja próbuję wyszydzić globalny requirei odkryć, że nawet to nie zadziała:

var path = require('path'),
    vm = require('vm'),
    fs = require('fs'),
    indexPath = path.join(__dirname, './underTest');

var globalRequire = require;

require = function(name) {
    console.log('require: ' + name);
    switch(name) {
        case 'connect':
        case indexPath:
            return globalRequire(name);
            break;
    }
};

Problem polega na tym, że requirefunkcja wewnątrz underTest.jspliku nie została w rzeczywistości wyszydzona. Nadal wskazuje na funkcję globalną require. Wygląda więc na to, że mogę wyszydzać requirefunkcję tylko w tym samym pliku, w którym robię mockowanie. Jeśli użyję globalnego requiredo uwzględnienia czegokolwiek, nawet po nadpisaniu lokalnej kopii, wymagane pliki nadal będą miały globalne requireodniesienie.


musisz nadpisać global.require. Zmienne zapisywane moduledomyślnie, ponieważ moduły mają zasięg modułu.
Raynos

@Raynos Jak miałbym to zrobić? global.require jest niezdefiniowane? Nawet jeśli zastąpię go własną funkcją, inne funkcje nigdy by tego nie używały, prawda?
HMR

Odpowiedzi:


175

Możesz teraz!

Opublikowałem proxyquire, który zajmie się nadpisaniem globalnego wymagania wewnątrz twojego modułu podczas testowania go.

Oznacza to, że nie potrzebujesz żadnych zmian w kodzie , aby wstrzyknąć makiety dla wymaganych modułów.

Proxyquire ma bardzo prosty interfejs API, który umożliwia rozwiązanie modułu, który próbujesz przetestować, i przekazanie makiet / kodów pośredniczących do wymaganych modułów w jednym prostym kroku.

@Raynos ma rację, że tradycyjnie musiałeś uciekać się do niezbyt idealnych rozwiązań, aby to osiągnąć lub zamiast tego rozwijać oddolny

To jest główny powód, dla którego stworzyłem proxyquire - aby umożliwić odgórne tworzenie sterowane testami bez żadnych kłopotów.

Zapoznaj się z dokumentacją i przykładami, aby ocenić, czy będzie pasować do Twoich potrzeb.


5
Używam proxyquire i nie mogę powiedzieć wystarczająco dobrych rzeczy. To mnie uratowało! Otrzymałem zadanie napisania testów węzłów jaśminowych dla aplikacji opracowanej w apceleratorze Titanium, który wymusza na niektórych modułach ścieżki bezwzględne i wiele zależności cyklicznych. proxyquire pozwolił mi przestać je przerwać i wyszydzić okrucieństwo, którego nie potrzebowałem do każdego testu. (Wyjaśniono tutaj ). Dziękuję bardzo!
Sukima,

Miło słyszeć, że proxyquire pomogło ci poprawnie przetestować kod :)
Thorsten Lorenz

1
bardzo fajny @ThorstenLorenz, będę pok. używać proxyquire!
bevacqua

Fantastyczny! Kiedy zobaczyłem zaakceptowaną odpowiedź, że „nie możesz”, pomyślałem „O Boże, poważnie ?!” ale to naprawdę go uratowało.
Chadwick

3
Dla tych z Was, którzy używają Webpack, nie trać czasu na badanie proxyquire. Nie obsługuje WebPacka. Zamiast tego zajmuję się programem ładującym do wstrzykiwań ( github.com/plasticine/inject-loader ).
Artif3x

116

Lepszą opcją w tym przypadku jest mockowanie metod zwracanego modułu.

Co więcej, większość modułów node.js jest singletonami; dwa fragmenty kodu, które wymagają () tego samego modułu, otrzymują to samo odwołanie do tego modułu.

Możesz to wykorzystać i użyć czegoś takiego jak sinon, aby wyszydzić wymagane przedmioty. test mokki następujący:

// in your testfile
var innerLib  = require('./path/to/innerLib');
var underTest = require('./path/to/underTest');
var sinon     = require('sinon');

describe("underTest", function() {
  it("does something", function() {
    sinon.stub(innerLib, 'toCrazyCrap').callsFake(function() {
      // whatever you would like innerLib.toCrazyCrap to do under test
    });

    underTest();

    sinon.assert.calledOnce(innerLib.toCrazyCrap); // sinon assertion

    innerLib.toCrazyCrap.restore(); // restore original functionality
  });
});

Sinon ma dobrą integrację z chai do tworzenia twierdzeń, a ja napisałem moduł integrujący sinon z mokką, aby umożliwić łatwiejsze czyszczenie szpiegów / stubów (aby uniknąć zanieczyszczenia testowego).

Zauważ, że underTest nie może być mockowany w ten sam sposób, ponieważ underTest zwraca tylko funkcję.

Inną opcją jest użycie Jest mocks. Śledź na ich stronie


1
Niestety, moduły node.js NIE są gwarantowane jako pojedyncze, jak wyjaśniono tutaj: justjs.com/posts/ ...
FrontierPsycho

4
@FrontierPsycho kilka rzeczy: Po pierwsze, jeśli chodzi o testowanie, artykuł jest nieistotny. Tak długo, jak testujesz swoje zależności (a nie zależności zależności), cały kod otrzyma ten sam obiekt z powrotem require('some_module'), ponieważ cały twój kod ma ten sam katalog node_modules. Po drugie, artykuł łączy przestrzeń nazw z singletonami, co jest trochę ortogonalne. Po trzecie, ten artykuł jest cholernie stary (jeśli chodzi o node.js), więc to, co mogło obowiązywać w tamtych czasach, prawdopodobnie nie jest aktualne.
Elliot Foster

2
Hm. O ile któryś z nas nie wykopie kodu, który udowadnia jeden lub drugi punkt, wybrałbym twoje rozwiązanie iniekcji zależności lub po prostu przekazując obiekty, jest to bezpieczniejsze i bardziej przyszłościowe.
FrontierPsycho

1
Nie jestem pewien, o co prosisz, aby zostało udowodnione. Powszechnie rozumie się pojedynczy (buforowany) charakter modułów węzłów. Wstrzykiwanie zależności, chociaż jest dobrą trasą, może oznaczać znacznie więcej kotłów i więcej kodu. DI jest bardziej powszechne w językach z typami statycznymi, gdzie trudniej jest dynamicznie wprowadzać szpiegów / stubów / mocków do kodu. Wiele projektów, które wykonałem w ciągu ostatnich trzech lat, wykorzystuje metodę opisaną w mojej odpowiedzi powyżej. To najłatwiejsza ze wszystkich metod, chociaż używam jej oszczędnie.
Elliot Foster

1
Proponuję poczytać o sinon.js. Jeśli używasz sinon (jak w powyższym przykładzie) byś albo innerLib.toCrazyCrap.restore()i restub lub zadzwoń sinon poprzez sinon.stub(innerLib, 'toCrazyCrap')który pozwala zmienić sposób zachowuje en: innerLib.toCrazyCrap.returns(false). Ponadto rewire wydaje się być bardzo podobny do proxyquirepowyższego rozszerzenia.
Elliot Foster

11

Używam mock-require . Upewnij się, że zdefiniowałeś swoje makiety przed requiretestowaniem modułu.


Również dobrze jest zrobić stop (<file>) lub stopAll () od razu, aby nie otrzymać buforowanego pliku w teście, w którym nie chcesz, aby makieta.
Justin Kruse,

1
To pomogło tonie.
wallop

2

Mocking requireto dla mnie nieprzyjemny hack. Osobiście starałbym się tego uniknąć i zmienić kod, aby był bardziej testowalny. Istnieją różne podejścia do obsługi zależności.

1) przekazywać zależności jako argumenty

function underTest(innerLib) {
    return innerLib.doComplexStuff();
}

Dzięki temu kod będzie uniwersalnie testowalny. Wadą jest to, że musisz przekazywać zależności, co może sprawić, że kod będzie wyglądał na bardziej skomplikowany.

2) zaimplementuj moduł jako klasę, a następnie użyj metod / właściwości klasy, aby uzyskać zależności

(To jest wymyślony przykład, w którym użycie klas nie jest rozsądne, ale przekazuje ideę) (przykład z ES6)

const innerLib = require('./path/to/innerLib')

class underTestClass {
    getInnerLib () {
        return innerLib
    }

    underTestMethod () {
        return this.getInnerLib().doComplexStuff()
    }
}

Teraz możesz łatwo getInnerLibprzetestować kod. Kod staje się bardziej szczegółowy, ale także łatwiejszy do przetestowania.


1
Nie sądzę, żeby to było oklepane, jak zakładasz ... to jest istota kpiny. Mockowanie wymaganych zależności czyni rzeczy tak prostymi, że daje kontrolę programistom bez zmiany struktury kodu. Twoje metody są zbyt szczegółowe i dlatego trudno o nich myśleć. Zamiast tego wybieram proxyrequire lub mock-require; nie widzę tutaj żadnego problemu. Kod jest przejrzysty i łatwy do zrozumienia i zapamiętania. Większość ludzi, którzy go czytają, napisała już kod, który chcesz, aby skomplikowali. Jeśli te biblioteki są hackerskie, to mocking i stubbing są również hakerskie z twojej definicji i powinny zostać zatrzymane.
Emmanuel Mahuni

1
Problem z podejściem nr 1 polega na tym, że przekazujesz wewnętrzne szczegóły implementacji na stos. W przypadku wielu warstw bycie konsumentem modułu staje się znacznie bardziej skomplikowane. Może jednak działać z podejściem podobnym do kontenera IOC, dzięki czemu zależności są automatycznie wstrzykiwane za Ciebie, jednak wydaje się, że mamy już zależności wstrzyknięte w modułach węzłów za pośrednictwem instrukcji importowania, więc sensowne jest, aby móc je mockować na tym poziomie .
magritte

1) To po prostu przenosi problem do innego pliku 2) nadal ładuje inny moduł, a tym samym narzuca narzut wydajności i prawdopodobnie powoduje efekty uboczne (takie jak popularny colorsmoduł, z którym się miesza String.prototype)
ThomasR

2

Jeśli kiedykolwiek używałeś żartu, prawdopodobnie znasz funkcję kpiny.

Używając "jest.mock (...)" możesz po prostu określić ciąg znaków, który wystąpiłby w instrukcji wymagania w twoim kodzie, a gdy moduł jest wymagany przy użyciu tego ciągu, zamiast tego zostanie zwrócony obiekt pozorowany.

Na przykład

jest.mock("firebase-admin", () => {
    const a = require("mocked-version-of-firebase-admin");
    a.someAdditionalMockedMethod = () => {}
    return a;
})

całkowicie zamieni wszystkie importy / wymagania "firebase-admin" na obiekt, który zwróciłeś z tej funkcji "fabryki".

Cóż, możesz to zrobić używając żart, ponieważ jest tworzy środowisko uruchomieniowe wokół każdego uruchamianego modułu i wstrzykuje "przechwyconą" wersję wymagania do modułu, ale nie byłbyś w stanie tego zrobić bez żartu.

Próbowałem to osiągnąć za pomocą pozornego wymagania, ale dla mnie nie działało to na zagnieżdżonych poziomach w moim źródle. Spójrz na następujący problem na githubie: mock-require nie zawsze wywoływany z Mocha .

Aby rozwiązać ten problem, stworzyłem dwa moduły npm, których możesz użyć do osiągnięcia tego, co chcesz.

Potrzebujesz jednej wtyczki babel i modułu mocker.

W swoim .babelrc użyj wtyczki babel-plugin-mock-require z następującymi opcjami:

...
"plugins": [
        ["babel-plugin-mock-require", { "moduleMocker": "jestlike-mock" }],
        ...
]
...

aw pliku testowym użyj modułu jestlike-mock w następujący sposób:

import {jestMocker} from "jestlike-mock";
...
jestMocker.mock("firebase-admin", () => {
            const firebase = new (require("firebase-mock").MockFirebaseSdk)();
            ...
            return firebase;
});
...

jestlike-mockModuł jest nadal bardzo elementarny i nie ma dużo dokumentacji, ale nie ma zbyt wiele kodu albo. Doceniam wszelkie PR za pełniejszy zestaw funkcji. Celem byłoby odtworzenie całej funkcji „jest.mock”.

Aby zobaczyć, jak jest to implementowane, można wyszukać kod w pakiecie "jest-runtime". Zobacz na przykład https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 , tutaj generują "automock" modułu.

Mam nadzieję, że to pomoże;)


1

Nie możesz. Musisz zbudować swój zestaw testów jednostkowych tak, aby najniższe moduły były testowane jako pierwsze, a moduły wyższego poziomu, które wymagają modułów, były testowane później.

Musisz także założyć, że każdy kod innej firmy i sam node.js są dobrze przetestowane.

Przypuszczam, że w najbliższej przyszłości pojawią się fałszywe frameworki, które nadpiszą global.require

Jeśli naprawdę musisz wstrzyknąć makietę, możesz zmienić kod, aby ujawnić zakres modułowy.

// underTest.js
var innerLib = require('./path/to/innerLib');

function underTest() {
    return innerLib.toCrazyCrap();
}

module.exports = underTest;
module.exports.__module = module;

// test.js
function test() {
    var underTest = require("underTest");
    underTest.__module.innerLib = {
        toCrazyCrap: function() { return true; }
    };
    assert.ok(underTest());
}

Ostrzegamy, że ujawnia się to .__modulew twoim API, a każdy kod może uzyskać dostęp do zakresu modułowego na własne niebezpieczeństwo.


2
Zakładanie, że kod innej firmy jest dobrze przetestowany, nie jest świetnym sposobem na pracę z IMO.
henry.oswald

5
@beck to świetny sposób na pracę. Zmusza cię to do pracy tylko z wysokiej jakości kodem stron trzecich lub pisania wszystkich fragmentów kodu, aby każda zależność była dobrze przetestowana
Raynos

Ok, myślałem, że chodziło Ci o nie przeprowadzanie testów integracji między Twoim kodem a kodem strony trzeciej. Zgoda.
henry.oswald

1
„Zestaw testów jednostkowych” to po prostu zbiór testów jednostkowych, ale testy jednostkowe powinny być od siebie niezależne, stąd jednostka w teście jednostkowym. Aby były użyteczne, testy jednostkowe powinny być szybkie i niezależne, tak aby można było wyraźnie zobaczyć, gdzie kod jest uszkodzony, gdy test jednostkowy nie powiedzie się.
Andreas Berheim Brudin

To nie zadziałało dla mnie. Obiekt modułu nie ujawnia "var innerLib ..." itp.
AnitKryst

1

Możesz użyć fałszywej biblioteki:

describe 'UnderTest', ->
  before ->
    mockery.enable( warnOnUnregistered: false )
    mockery.registerMock('./path/to/innerLib', { doComplexStuff: -> 'Complex result' })
    @underTest = require('./path/to/underTest')

  it 'should compute complex value', ->
    expect(@underTest()).to.eq 'Complex result'

1

Prosty kod do mockowania modułów dla ciekawskich

Zauważ części gdzie manipulować require.cachei uwaga require.resolvemetodę, gdyż jest to tajny sos.

class MockModules {  
  constructor() {
    this._resolvedPaths = {} 
  }
  add({ path, mock }) {
    const resolvedPath = require.resolve(path)
    this._resolvedPaths[resolvedPath] = true
    require.cache[resolvedPath] = {
      id: resolvedPath,
      file: resolvedPath,
      loaded: true,
      exports: mock
    }
  }
  clear(path) {
    const resolvedPath = require.resolve(path)
    delete this._resolvedPaths[resolvedPath]
    delete require.cache[resolvedPath]
  }
  clearAll() {
    Object.keys(this._resolvedPaths).forEach(resolvedPath =>
      delete require.cache[resolvedPath]
    )
    this._resolvedPaths = {}
  }
}

Użyj jak :

describe('#someModuleUsingTheThing', () => {
  const mockModules = new MockModules()
  beforeAll(() => {
    mockModules.add({
      // use the same require path as you normally would
      path: '../theThing',
      // mock return an object with "theThingMethod"
      mock: {
        theThingMethod: () => true
      }
    })
  })
  afterAll(() => {
    mockModules.clearAll()
  })
  it('should do the thing', async () => {
    const someModuleUsingTheThing = require('./someModuleUsingTheThing')
    expect(someModuleUsingTheThing.theThingMethod()).to.equal(true)
  })
})

ALE ... proxyquire jest całkiem niezłe i powinieneś tego używać. Utrzymuje twoje zastąpienia wymagań zlokalizowane tylko do testów i bardzo to polecam.

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.