Comment tester unitaire un module Node.js qui nécessite d'autres modules et comment simuler la fonction globale require?


156

Ceci est un exemple trivial qui illustre le nœud de mon problème:

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

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

module.exports = underTest;

J'essaye d'écrire un test unitaire pour ce code. Comment puis-je simuler l'exigence de la innerLibsans se moquer complètement de la requirefonction?

Donc, c'est moi qui essaie de me moquer du monde requireet de découvrir que cela ne fonctionnera même pas pour faire cela:

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

Le problème est que la requirefonction à l'intérieur du underTest.jsfichier n'a en fait pas été simulée. Il pointe toujours vers la requirefonction globale . Il semble donc que je ne puisse simuler la requirefonction que dans le même fichier dans lequel je me moque. Si j'utilise le global requirepour inclure quoi que ce soit, même après avoir remplacé la copie locale, les fichiers requis auront toujours le requireréférence mondiale .


vous devez écraser global.require. Les variables écrites modulepar défaut car les modules ont une portée de module.
Raynos

@Raynos Comment pourrais-je faire ça? global.require n'est pas défini? Même si je le remplace par ma propre fonction, d'autres fonctions ne l'utiliseront jamais?
HMR

Réponses:


175

Tu peux maintenant!

J'ai publié proxyquire qui se chargera de remplacer le requis global dans votre module pendant que vous le testez.

Cela signifie que vous n'avez pas besoin de modifier votre code pour injecter des simulations pour les modules requis.

Proxyquire a une API très simple qui permet de résoudre le module que vous essayez de tester et de transmettre des simulacres / stubs pour ses modules requis en une seule étape.

@Raynos a raison de dire que traditionnellement, vous deviez recourir à des solutions pas très idéales pour y parvenir ou faire du développement ascendant à la place

C'est la raison principale pour laquelle j'ai créé proxyquire - pour permettre un développement de haut en bas piloté par des tests sans aucun problème.

Jetez un œil à la documentation et aux exemples afin de déterminer s'il répondra à vos besoins.


5
J'utilise proxyquire et je ne peux pas dire assez de bonnes choses. Cela m'a sauvé! J'ai été chargé d'écrire des tests de nœuds jasmin pour une application développée dans appcelerator Titanium qui oblige certains modules à être des chemins absolus et de nombreuses dépendances circulaires. proxyquire m'a permis de combler ces lacunes et de simuler la cruauté dont je n'avais pas besoin pour chaque test. (Expliqué ici ). Merci beaucoup!
Sukima

Heureux d'apprendre que proxyquire vous a aidé à tester votre code correctement :)
Thorsten Lorenz

1
très sympa @ThorstenLorenz, je vais déf. utiliser proxyquire!
bevacqua

Fantastique! Quand j'ai vu la réponse acceptée que "vous ne pouvez pas", j'ai pensé "Oh mon Dieu, sérieusement?!" mais cela l'a vraiment sauvé.
Chadwick

3
Pour ceux d'entre vous qui utilisent Webpack, ne passez pas de temps à rechercher proxyquire. Il ne prend pas en charge Webpack. Je cherche plutôt inject -loader ( github.com/plasticine/inject-loader ).
Artif3x

116

Une meilleure option dans ce cas est de simuler les méthodes du module renvoyé.

Pour le meilleur ou pour le pire, la plupart des modules node.js sont des singletons; deux morceaux de code qui nécessitent () le même module obtiennent la même référence à ce module.

Vous pouvez en tirer parti et utiliser quelque chose comme sinon pour simuler les éléments requis. Le test de moka suit:

// 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 a une bonne intégration avec chai pour faire des assertions, et j'ai écrit un module pour intégrer sinon avec mocha pour permettre un nettoyage plus facile des espions / stub (pour éviter la pollution des tests.)

Notez que underTest ne peut pas être simulé de la même manière, car underTest ne renvoie qu'une fonction.

Une autre option consiste à utiliser des simulations Jest. Suivez leur page


1
Malheureusement, les modules node.js ne sont PAS garantis d'être des singletons, comme expliqué ici: justjs.com/posts
FrontierPsycho

4
@FrontierPsycho quelques petites choses: Premièrement, en ce qui concerne les tests, l'article n'est pas pertinent. Tant que vous testez vos dépendances (et non les dépendances de dépendances), tout votre code récupérera le même objet lorsque vous require('some_module'), car tout votre code partage le même répertoire node_modules. Deuxièmement, l'article confond l'espace de noms avec des singletons, ce qui est en quelque sorte orthogonal. Troisièmement, cet article est sacrément vieux (en ce qui concerne node.js), donc ce qui aurait pu être valide à l'époque n'est peut-être pas valide maintenant.
Elliot Foster

2
Hm. À moins que l'un de nous ne déterre réellement du code qui prouve un point ou un autre, j'irais avec votre solution d'injection de dépendances, ou simplement en passant des objets, c'est plus sûr et plus à l'épreuve du temps.
FrontierPsycho

1
Je ne sais pas ce que vous demandez pour être prouvé. La nature singleton (mise en cache) des modules de nœuds est généralement comprise. L'injection de dépendance, bien qu'une bonne voie, peut être beaucoup plus de plaque chauffante et plus de code. La DI est plus courante dans les langages à typage statique, où il est plus difficile de poinçonner dynamiquement des espions / stubs / mocks dans votre code. Plusieurs projets que j'ai réalisés au cours des trois dernières années utilisent la méthode décrite dans ma réponse ci-dessus. C'est la plus simple de toutes les méthodes, même si je l'utilise avec parcimonie.
Elliot Foster

1
Je vous suggère de lire sur sinon.js. Si vous utilisez sinon (comme dans l'exemple ci - dessus) , vous soit innerLib.toCrazyCrap.restore()et restub, ou appelez via sinon ce sinon.stub(innerLib, 'toCrazyCrap')qui vous permet de changer la façon dont les stub se comporte: innerLib.toCrazyCrap.returns(false). De plus, rewire semble être très similaire à l' proxyquireextension ci-dessus.
Elliot Foster

11

j'utilise mock-require . Assurez-vous de définir vos simulacres avant requirele module à tester.


Il est également bon de faire stop (<file>) ou stopAll () tout de suite pour ne pas obtenir un fichier mis en cache dans un test où vous ne voulez pas le simulacre.
Justin Kruse

1
Cela a aidé une tonne.
wallop

2

Se moquer requireme semble être un vilain hack. J'essaierais personnellement de l'éviter et de refactoriser le code pour le rendre plus testable. Il existe différentes approches pour gérer les dépendances.

1) Passer les dépendances comme arguments

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

Cela rendra le code testable universellement. L'inconvénient est que vous devez passer des dépendances, ce qui peut rendre le code plus compliqué.

2) implémentez le module en tant que classe, puis utilisez les méthodes / propriétés de classe pour obtenir les dépendances

(Ceci est un exemple artificiel, où l'utilisation de classe n'est pas raisonnable, mais elle transmet l'idée) (exemple ES6)

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

class underTestClass {
    getInnerLib () {
        return innerLib
    }

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

Vous pouvez maintenant facilement getInnerLibtester la méthode stub pour tester votre code. Le code devient plus détaillé, mais aussi plus facile à tester.


1
Je ne pense pas que ce soit piraté comme vous le supposez ... c'est l'essence même de la moquerie. La simulation des dépendances requises rend les choses si simples qu'elles donnent le contrôle au développeur sans changer la structure du code. Vos méthodes sont trop verbeuses et donc difficiles à raisonner. Je choisis proxyrequire ou mock-require à ce sujet; je ne vois aucun problème ici. Le code est clair et facile à raisonner et se souvient que la plupart des gens qui lisent ceci ont déjà écrit du code que vous voulez qu'ils compliquent. Si ces bibliothèques sont piratées, alors les moqueries et les stubbing sont également piratés par votre définition et doivent être arrêtés.
Emmanuel Mahuni

1
Le problème avec l'approche n ° 1 est que vous transmettez les détails de l'implémentation interne dans la pile. Avec plusieurs couches, il devient alors beaucoup plus compliqué d'être un consommateur de votre module. Cependant, cela peut fonctionner avec une approche de type conteneur IOC afin que les dépendances soient automatiquement injectées pour vous, mais c'est comme si nous avons déjà des dépendances injectées dans des modules de nœuds via l'instruction importations, alors il est logique de pouvoir les simuler à ce niveau .
magritte

1) Cela déplace simplement le problème vers un autre fichier 2) charge toujours l'autre module et impose ainsi une surcharge de performances, et provoque éventuellement des effets secondaires (comme le colorsmodule populaire qui String.prototype
dérange

2

Si vous avez déjà utilisé jest, vous êtes probablement familier avec la fonction simulée de jest.

En utilisant "jest.mock (...)", vous pouvez simplement spécifier la chaîne qui apparaîtrait dans une instruction require dans votre code quelque part et chaque fois qu'un module est requis en utilisant cette chaîne, un objet-maquette serait renvoyé à la place.

Par exemple

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

remplacerait complètement toutes les importations / demandes de "firebase-admin" par l'objet que vous avez renvoyé de cette fonction "usine".

Eh bien, vous pouvez le faire lorsque vous utilisez jest car jest crée un runtime autour de chaque module qu'il exécute et injecte une version "accrochée" de require dans le module, mais vous ne pourriez pas le faire sans plaisanter.

J'ai essayé d'y parvenir avec mock-require, mais pour moi, cela ne fonctionnait pas pour les niveaux imbriqués dans ma source. Jetez un œil au problème suivant sur github: mock-require pas toujours appelé avec Mocha .

Pour résoudre ce problème, j'ai créé deux modules npm que vous pouvez utiliser pour réaliser ce que vous voulez.

Vous avez besoin d'un babel-plugin et d'un module mocker.

Dans votre .babelrc, utilisez le plugin babel-plugin-mock-require avec les options suivantes:

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

et dans votre fichier de test, utilisez le module jestlike-mock comme ceci:

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

Le jestlike-mockmodule est encore très rudimentaire et n'a pas beaucoup de documentation mais il n'y a pas beaucoup de code non plus. J'apprécie tous les PR pour un ensemble de fonctionnalités plus complet. Le but serait de recréer toute la fonctionnalité "jest.mock".

Afin de voir comment jest implémente cela, on peut rechercher le code dans le package "jest-runtime". Voir https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/index.js#L734 par exemple, ici ils génèrent un "automock" d'un module.

J'espère que cela pourra aider ;)


1

Vous ne pouvez pas. Vous devez construire votre suite de tests unitaires afin que les modules les plus bas soient testés en premier et que les modules de niveau supérieur qui nécessitent des modules soient testés ensuite.

Vous devez également supposer que tout code tiers et node.js lui-même sont bien testés.

Je suppose que vous verrez arriver dans un proche avenir des frameworks moqueurs qui écrasent global.require

Si vous devez vraiment injecter une maquette, vous pouvez modifier votre code pour exposer une portée modulaire.

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

Soyez averti que cela expose .__moduledans votre API et tout code peut accéder à la portée modulaire à son propre danger.


2
En supposant que le code tiers est bien testé, ce n'est pas un excellent moyen de travailler avec l'OMI.
henry.oswald

5
@beck c'est une excellente façon de travailler. Cela vous oblige à ne travailler qu'avec du code tiers de haute qualité ou à écrire tous les morceaux de votre code afin que chaque dépendance soit bien testée
Raynos

Ok, je pensais que vous parliez de ne pas faire de tests d'intégration entre votre code et le code tiers. D'accord.
henry.oswald

1
Une "suite de tests unitaires" est juste une collection de tests unitaires, mais les tests unitaires doivent être indépendants les uns des autres, d'où l'unité en test unitaire. Pour être utilisables, les tests unitaires doivent être rapides et indépendants, de sorte que vous puissiez voir clairement où le code est cassé lorsqu'un test unitaire échoue.
Andreas Berheim Brudin

Cela n'a pas fonctionné pour moi. L'objet module n'expose pas le "var innerLib ..." etc.
AnitKryst

1

Vous pouvez utiliser la bibliothèque de moquerie :

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

Code simple pour simuler des modules pour les curieux

Remarquez les parties où vous manipulez la méthode require.cacheet note require.resolvecar c'est la sauce secrète.

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 = {}
  }
}

Utilisez comme :

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

MAIS ... proxyquire est assez génial et vous devriez l'utiliser. Il maintient vos exigences de remplacement localisées aux tests uniquement et je le recommande vivement.

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.