web-dev-qa-db-fra.com

Comment se moquer de fonctions dans le même module en utilisant jest

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

Le problème est que, après l’importation, foo conserve la référence à la bar originale.

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

Merci!

EDIT: 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 p. moche à mon avis à faire partout: /

33
Mark

pour commencer, la solution sur laquelle j’ai opté était d’utiliser injection de dépendance , en définissant un argument par défaut.

Donc je changerais

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

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

à

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

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

Il ne s’agit pas d’un changement radical 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 l'avantage de conduire à un code de test légèrement plus agréable aussi :)

4
Mark

Le problème semble être lié à la manière dont vous vous attendez à ce que l'étendue de la barre soit résolue. 

D'une part, dans 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 fonctions).

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

Lorsque vous pensez que vous avez remplacé la fonction barre par une nouvelle, vous venez de remplacer la copie de référence dans le cadre de votre module.test.js

Pour que foo utilise réellement une autre version de bar, vous avez deux possibilités: 

  1. Dans module.js, exportez une classe ou une instance en maintenant 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 this mot clé 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 conteneur global exports. Ce n'est pas une méthode recommandée, car vous risquez d'introduire des comportements étranges dans d'autres tests si vous ne réinitialisez pas correctement les exportations à leur état initial.

12
John-Philip

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. Comme ça:

import * as thisModule from './module';

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

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

Se moquer de bar est vraiment 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');
    });
});

Importer le module dans son propre code semble étrange, mais grâce au support de l'ES6 pour les importations cycliques, cela fonctionne vraiment bien.

9
MostafaR

Si vous définissez vos exportations, vous pouvez ensuite référencer vos fonctions dans l’objet exportations. Ensuite, vous pouvez écraser les fonctions de vos modèles 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
Sean

J'ai eu le même problème et, en raison des normes anti-pelucheuses du projet, définir une classe ou réécrire les références dans la variable exports n'était pas une option approuvable pour la révision de code, même si les définitions de linting ne l'avaient pas empêché. Ce que je suis tombé sur une option viable est d’utiliser le plugin babel-rewire qui est beaucoup plus propre, du moins en apparence. Alors que je trouvais cela utilisé dans un autre projet auquel j'avais accès, j'ai remarqué que c'était déjà dans une réponse à une question similaire que j'ai liée ici . Ceci est un extrait ajusté pour 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 des espions car 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

0
Brandon Hunter

Travaille pour moi:

cat moduleWithFunc.ts

export function funcA() {
 return export.funcB();
}
export function funcB() {
 return false;
}

cat moduleWithFunc.test.ts

import * as module from './moduleWithFunc';

describe('testFunc', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  afterEach(() => {
    module.funcB.mockRestore();
  });

  it.only('testCase', () => {
    // arrange
    jest.spyOn(module, 'funcB').mockImplementationOnce(jest.fn().mockReturnValue(true));

    // act
    const result = module.funcA();

    // assert
    expect(result).toEqual(true);
    expect(module.funcB).toHaveBeenCalledTimes(1);
  });
});