141 votes

Comment simuler des fonctions dans le même module en utilisant Jest ?

Quelle est la meilleure façon de simuler correctement l'exemple suivant ?

Le problème est qu'après le temps d'importation, foo conserve la référence à l'original non moqué bar .

module.js :

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

module.test.js :

import * as module from '../src/module';

describe('module', () => {
    let barSpy;

    beforeEach(() => {
        barSpy = jest.spyOn(
            module,
            'bar'
        ).mockImplementation(jest.fn());
    });

    afterEach(() => {
        barSpy.mockRestore();
    });

    it('foo', () => {
        console.log(jest.isMockFunction(module.bar)); // outputs true

        module.bar.mockReturnValue('fake bar');

        console.log(module.bar()); // outputs 'fake bar';

        expect(module.foo()).toEqual('I am foo. bar is fake bar');
        /**
         * does not work! we get the following:
         *
         *  Expected value to equal:
         *    "I am foo. bar is fake bar"
         *  Received:
         *    "I am foo. bar is bar"
         */
    });
});

Je pourrais changer :

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

à :

export function foo () {
    return `I am foo. bar is ${exports.bar()}`;
}

mais c'est assez laid à mon avis de le faire partout.

9 votes

Voir le fil de discussion sur jest Page GH github.com/facebook/jest/issues/936#issuecomment-545080082

3 votes

En 2021, Jest dispose d'une méthode officielle pour réaliser ce "partial mocking", qui ne nécessite pas la modification de l'élément module.js et est simple et simple/déclaratif à écrire : jestjs.io/docs/mock-functions#mocking-partials .

59voto

MostafaR Points 1832

Une autre solution consiste à importer le module dans son propre fichier de code et à utiliser l'instance importée de toutes les entités exportées. Voici comment procéder :

import * as thisModule from './module';

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${thisModule.bar()}`;
}

Se moquer maintenant bar est très facile, car foo utilise également l'instance exportée de bar :

import * as module from '../src/module';

describe('module', () => {
    it('foo', () => {
        spyOn(module, 'bar').and.returnValue('fake bar');
        expect(module.foo()).toEqual('I am foo. bar is fake bar');
    });
});

L'importation du module dans son propre code semble étrange, mais grâce à la prise en charge des importations cycliques par ES6, cela fonctionne très bien.

1 votes

Cela a fonctionné pour moi, avec le moins d'impact possible sur le code existant, et des tests faciles à suivre.

2 votes

Pour moi aussi, c'était la voie la plus facile.

0 votes

Très utile. Je vous remercie.

48voto

John-Philip Points 1402

Le problème semble être lié à la manière dont vous envisagez de résoudre la question de la portée de la barre.

D'une part, en module.js vous exportez deux fonctions (au lieu d'un objet contenant ces deux fonctions). En raison de la manière dont les modules sont exportés, la référence au conteneur des éléments exportés est exports comme vous l'avez mentionné.

D'autre part, vous gérez votre exportation (que vous avez aliasée module ) comme un objet contenant ces fonctions et essayant de remplacer l'une de ses fonctions (la barre de fonction).

Si vous regardez de près votre implémentation de foo, vous tenez en fait une référence fixe à la fonction bar.

Lorsque vous pensez avoir remplacé la fonction bar par une nouvelle, vous avez en fait remplacé la copie de référence dans la portée de votre module.test.js.

Pour que foo utilise effectivement une autre version de bar, deux possibilités s'offrent à vous :

  1. Dans module.js, exportez une classe ou une instance contenant les méthodes foo et bar :

    Module.js :

    export class MyModule {
      function bar () {
        return 'bar';
      }
    
      function foo () {
        return `I am foo. bar is ${this.bar()}`;
      }
    }

    Notez l'utilisation de cette dans la méthode foo.

    Module.test.js :

    import { MyModule } from '../src/module'
    
    describe('MyModule', () => {
      //System under test :
      const sut:MyModule = new MyModule();
    
      let barSpy;
    
      beforeEach(() => {
          barSpy = jest.spyOn(
              sut,
              'bar'
          ).mockImplementation(jest.fn());
      });
    
      afterEach(() => {
          barSpy.mockRestore();
      });
    
      it('foo', () => {
          sut.bar.mockReturnValue('fake bar');
          expect(sut.foo()).toEqual('I am foo. bar is fake bar');
      });
    });
  2. Comme vous l'avez dit, réécrivez la référence globale dans le fichier global exports contenant. Cette méthode n'est pas recommandée, car vous risquez d'introduire des comportements bizarres dans d'autres tests si vous ne réinitialisez pas correctement les exportations à leur état initial.

13voto

Mark Points 8608

Par ailleurs, la solution que j'ai retenue est d'utiliser l'injection de dépendance en définissant un argument par défaut.

Je changerais donc

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

a

export function bar () {
    return 'bar';
}

export function foo (_bar = bar) {
    return `I am foo. bar is ${_bar()}`;
}

Il ne s'agit pas d'une modification radicale de l'API de mon composant, et je peux facilement remplacer la barre dans mon test en procédant comme suit

import { foo, bar } from '../src/module';

describe('module', () => {
    it('foo', () => {
        const dummyBar = jest.fn().mockReturnValue('fake bar');
        expect(foo(dummyBar)).toEqual('I am foo. bar is fake bar');
    });
});

Cela a aussi l'avantage de conduire à un code de test un peu plus agréable :)

10 votes

Je ne suis généralement pas un fan de l'injection de dépendance, puisque vous permettez aux tests de modifier la façon dont le code est écrit. Cela dit, c'est mieux que la réponse actuelle la plus votée, qui est plutôt moche

17 votes

Test plus sympa mais mauvais code. Ce n'est pas vraiment une bonne idée de changer votre code parce que vous ne pouvez pas trouver un moyen de le tester. En tant que développeur, lorsque je regarde ce code, je me demande 100 fois pourquoi une méthode particulière présente dans le module est passée en tant que dépendance à une autre méthode dans le même module.

10voto

Brandon Hunter Points 45

J'ai eu le même problème et en raison des normes de linting du projet, la définition d'une classe ou la réécriture des références dans le fichier exports n'étaient pas des options approuvables par la revue de code, même si elles n'étaient pas empêchées par les définitions de linting. Ce que j'ai trouvé comme option viable est d'utiliser l'option babel-rewire-plugin qui est beaucoup plus propre, du moins en apparence. J'ai trouvé cette méthode dans un autre projet auquel j'avais accès, mais j'ai remarqué qu'elle figurait déjà dans une réponse à une question similaire que j'ai mise en lien. ici . Il s'agit d'un extrait adapté à cette question (et sans utiliser d'espions) fourni à partir de la réponse liée pour référence (j'ai également ajouté des points-virgules en plus de supprimer les espions parce que je ne suis pas un païen) :

import __RewireAPI__, * as module from '../module';

describe('foo', () => {
  it('calls bar', () => {
    const barMock = jest.fn();
    __RewireAPI__.__Rewire__('bar', barMock);

    module.foo();

    expect(bar).toHaveBeenCalledTimes(1);
  });
});

https://stackoverflow.com/a/45645229/6867420

5 votes

Cette réponse devrait être acceptée. Le plugin fonctionne simplement && il n'y a aucun besoin de réécrire du code en dehors du test. TY

0 votes

Merci pour cette information, si vous êtes dans un environnement babel, c'est la réponse que vous cherchez.

1voto

Sean Points 681

Si vous définissez vos exportations, vous pouvez alors faire référence à vos fonctions en tant que partie de l'objet d'exportation. Vous pouvez alors écraser les fonctions dans vos objets fantaisie individuellement. Cela est dû au fait que l'importation fonctionne comme une référence et non comme une copie.

module.js :

exports.bar () => {
    return 'bar';
}

exports.foo () => {
    return `I am foo. bar is ${exports.bar()}`;
}

module.test.js :

describe('MyModule', () => {

  it('foo', () => {
    let module = require('./module')
    module.bar = jest.fn(()=>{return 'fake bar'})

    expect(module.foo()).toEqual('I am foo. bar is fake bar');
  });

})

1 votes

J'aime bien cela, mais pour moi, cela explose dans la liasse de production. exports is undefiend

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X