207 votes

Comment changer l'implémentation simulée sur une base de test individuel ?

Je voudrais modifier la mise en œuvre d'une dépendance moquée sur une base de test unique en étendant le comportement par défaut du mock et en le ramenant à la mise en œuvre d'origine lorsque le test suivant s'exécute.

Plus brièvement, voici ce que j'essaie d'accomplir :

  1. Mocker la dépendance
  2. Modifier/étendre la mise en œuvre du mock dans un seul test
  3. Revenir à la mise en œuvre initiale du mock lorsque le test suivant s'exécute

Je utilise actuellement Jest v21. Voici à quoi ressemblerait un test typique :

// __mocks__/myModule.js

const myMockedModule = jest.genMockFromModule('../myModule');

myMockedModule.a = jest.fn(() => true);
myMockedModule.b = jest.fn(() => true);

export default myMockedModule;

// __tests__/myTest.js

import myMockedModule from '../myModule';

// Mock myModule
jest.mock('../myModule');

beforeEach(() => {
  jest.clearAllMocks();
});

describe('MyTest', () => {
  it('devrait tester avec le mock par défaut', () => {
    myMockedModule.a(); // === true
    myMockedModule.b(); // === true
  });

  it('devrait remplacer le résultat du mock myMockedModule.b (et laisser les autres méthodes intactes)', () => {
    // Étendre le changement du mock
    myMockedModule.a(); // === true
    myMockedModule.b(); // === 'surchargé'
    // Restaurer le mock à la mise en œuvre d'origine sans effets secondaires
  });

  it('devrait revenir au mock par défaut de myMockedModule', () => {
    myMockedModule.a(); // === true
    myMockedModule.b(); // === true
  });
});

Voici ce que j'ai essayé jusqu'à présent :

  1. mockFn.mockImplementationOnce(fn)

    it('devrait remplacer le résultat du mock myModule.b (et laisser les autres méthodes intactes)', () => {
    
      myMockedModule.b.mockImplementationOnce(() => 'surchargé');
    
      myModule.a(); // === true
      myModule.b(); // === 'surchargé'
    });

    Avantages

    • Reviens à la mise en œuvre d'origine après le premier appel

    Inconvénients

    • Il casse si le test appelle b plusieurs fois
    • Il ne revient pas à la mise en œuvre originale tant que b n'est pas appelé (fuite dans le test suivant)
  2. jest.doMock(moduleName, factory, options)

    it('should override myModule.b mock result (and leave the other methods untouched)', () => {
    
      jest.doMock('../myModule', () => {
        return {
          a: jest.fn(() => true,
          b: jest.fn(() => 'overridden',
        }
      });
    
      myModule.a(); // === true
      myModule.b(); // === 'overridden'
    });

    Avantages

    • Re-mocks explicitement à chaque test

    Inconvénients

    • Impossible de définir une mise en œuvre par défaut pour tous les tests
    • Impossible d'étendre l'implémentation par défaut obligeant à redéclarer chaque méthode moquée
  3. Mocking manuel avec des méthodes setter (comme expliqué ici)

    // __mocks__/myModule.js
    
    const myMockedModule = jest.genMockFromModule('../myModule');
    
    let a = true;
    let b = true;
    
    myMockedModule.a = jest.fn(() => a);
    myMockedModule.b = jest.fn(() => b);
    
    myMockedModule.__setA = (value) => { a = value };
    myMockedModule.__setB = (value) => { b = value };
    myMockedModule.__reset = () => {
      a = true;
      b = true;
    };
    export default myMockedModule;
    
    // __tests__/myTest.js
    
    it('devrait remplacer le résultat du mock myModule.b (et laisser les autres méthodes intactes)', () => {
      myModule.__setB('surchargé');
    
      myModule.a(); // === true
      myModule.b(); // === 'surchargé'
    
      myModule.__reset();
    });

    Avantages

    • Contrôle total sur les résultats moqués

    Inconvénients

    • Beaucoup de code redondant
    • Difficile à maintenir à long terme
  4. jest.spyOn(object, methodName)

    beforeEach(() => {
      jest.clearAllMocks();
      jest.restoreAllMocks();
    });
    
    // Mock myModule
    jest.mock('../myModule');
    
    it('devrait remplacer le résultat du mock myModule.b (et laisser les autres méthodes intactes)', () => {
    
      const spy = jest.spyOn(myMockedModule, 'b').mockImplementation(() => 'surchargé');
    
      myMockedModule.a(); // === true
      myMockedModule.b(); // === 'surchargé'
    
      // Comment revenir à la valeur moquée d'origine ?
    });

    Inconvénients

    • Je ne peux pas revenir à mockImplementation à la valeur de retour moquée d'origine, affectant donc les tests suivants

1 votes

Bien. Mais comment faites-vous l'option 2 pour un module npm comme '@private-repo/module'? La plupart des exemples que je vois ont des chemins relatifs? Est-ce que cela fonctionne aussi pour les modules installés?

111voto

A Jar of Clay Points 585

Utilisez mockFn.mockImplementation(fn).

import { funcToMock } from './somewhere';
jest.mock('./somewhere');

beforeEach(() => {
  funcToMock.mockImplementation(() => { /* implémentation par défaut */ });
  // (funcToMock as jest.Mock)... en TS
});

test('cas nécessitant une implémentation différente de funcToMock', () => {
  funcToMock.mockImplementation(() => { /* implémentation spécifique à ce test */ });
  // (funcToMock as jest.Mock)... en TS

  // ...
});

0 votes

Cela a fonctionné pour moi lorsque je moquais la fonction format de date-fns-tz.

1 votes

Cela devrait être la réponse acceptée

0 votes

Je n'aime pas cette réponse car cela nécessite de dupliquer la logique de la fonction que vous voulez mocker. Si vous modifiez la fonction source, vous devez maintenant penser à mettre à jour chaque classe de test qui se moque de son implémentation avec sa logique réelle.

88voto

user1095118 Points 1545

Un joli modèle pour écrire des tests est de créer une fonction factory de configuration qui renvoie les données dont vous avez besoin pour tester le module actuel.

Voici un exemple de code suivant votre deuxième exemple bien qu'il permette la fourniture de valeurs par défaut et de remplacement de manière réutilisable.

const spyReturns = returnValue => jest.fn(() => returnValue);

describe("scénario", () => {
  beforeEach(() => {
    jest.resetModules();
  });

  const setup = (mockOverrides) => {
    const mockedFunctions =  {
      a: spyReturns(true),
      b: spyReturns(true),
      ...mockOverrides
    }
    jest.doMock('../myModule', () => mockedFunctions)
    return {
      mockedModule: require('../myModule')
    }
  }

  it("devrait renvoyer true pour le module a", () => {
    const { mockedModule } = setup();
    expect(mockedModule.a()).toEqual(true)
  });

  it("devrait renvoyer le remplacement pour le module a", () => {
    const EXPECTED_VALUE = "remplacement"
    const { mockedModule } = setup({ a: spyReturns(EXPECTED_VALUE)});
    expect(mockedModule.a()).toEqual(EXPECTED_VALUE)
  });
});

Il est important de noter que vous devez réinitialiser les modules qui ont été mis en cache en utilisant jest.resetModules(). Ceci peut être fait dans beforeEach ou une fonction de nettoyage similaire.

Voir la documentation de l'objet jest pour plus d'informations : https://jestjs.io/docs/jest-object.

0 votes

Cela ne fonctionne pas pour moi en ce moment. Dans votre cas, mockedModule renvoie mockedModule: typeof jest.a() est undefined. Au lieu de renvoyer des choses comme advanceTimersByTime, clearMocks, resetAllMocks etc..

0 votes

@ronnyrr, vrai, vous devez utiliser require(...) pour obtenir le module simulé après avoir appelé jest.doMock(). De plus, vous devez appeler jest.resetModules() à l'intérieur de beforeEach() afin que vos appels ultérieurs à require(...) utilisent la version simulée "la plus récente" d'un module. J'ai soumis une modification à la réponse pour montrer cela dans le code également.

60voto

Dr Tom Points 36

Un peu en retard pour la fête, mais si quelqu'un d'autre rencontre des problèmes avec ça.

Nous utilisons TypeScript, ES6 et babel pour le développement react-native.

Nous simulons généralement des modules externes NPM dans le répertoire racine __mocks__.

Je voulais remplacer une fonction spécifique d'un module dans la classe Auth de aws-amplify pour un test spécifique.

    import { Auth } from 'aws-amplify';
    import GetJwtToken from './GetJwtToken';
    ...
    it('Quand idToken devrait renvoyer "123"', async () => {
      const spy = jest.spyOn(Auth, 'currentSession').mockImplementation(() => ({
        getIdToken: () => ({
          getJwtToken: () => '123',
        }),
      }));

      const result = await GetJwtToken();
      expect(result).toBe('123');
      spy.mockRestore();
    });

Gist: https://gist.github.com/thomashagstrom/e5bffe6c3e3acec592201b6892226af2

Tutoriel: https://medium.com/p/b4ac52a005d#19c5

0 votes

C'était la seule chose qui a fonctionné pour moi, avec le moins de code superflu possible. Dans mon scénario, j'avais une exportation nommée en TypeScript à partir d'un package sans exportation par défaut, j'ai donc fini par utiliser import * as MyModule; puis const { useQuery } = MyModule pour pouvoir toujours utiliser les imports de la même manière sans faire MyModule.someExport partout.

1 votes

Le medium article mentionné ici est en or :)

4voto

Denis P Points 58

Lorsque l'on se moque d'une seule méthode (lorsqu'il est nécessaire de laisser le reste de l'implémentation d'une classe/module intacte), j'ai découvert que l'approche suivante était utile pour réinitialiser toutes les modifications de l'implémentation individuelle des tests.

J'ai trouvé cette approche la plus concise, sans besoin de jest.mock quelque chose au début du fichier, etc. Vous avez juste besoin du code que vous voyez ci-dessous pour se moquer de MyClass.methodName. Un autre avantage est que par défaut, spyOn conserve l'implémentation de la méthode originale tout en sauvegardant toutes les statistiques (nombre d'appels, arguments, résultats, etc.) à tester, et conserver l'implémentation par défaut est incontournable dans certains cas. Vous avez donc la flexibilité de conserver l'implémentation par défaut ou de la modifier avec une simple addition de .mockImplementation comme mentionné dans le code ci-dessous.

Le code est en Typescript avec des commentaires soulignant la différence pour JS (la différence se trouve dans une seule ligne, pour être précis). Testé avec Jest 26.6.

describe('ensemble de tests', () => {
    let mockedFn: jest.SpyInstance; // void est la valeur de retour de la fonction moquée, à changer si nécessaire
    // Pour JS brut, utilisez simplement : let mockedFn;

    beforeEach(() => {
        mockedFn = jest.spyOn(MyClass.prototype, 'methodName');
        // Utilisez ce qui suit si vous devez non seulement espionner mais aussi remplacer l'implémentation de la méthode par défaut :
        // mockedFn = jest.spyOn(MyClass.prototype, 'methodName').mockImplementation(() => {/*implémentation personnalisée*/});
    });

    afterEach(() => {
        // Réinitialiser à l'implémentation de la méthode d'origine (non moquée) et effacer toutes les données moquées
        mockedFn.mockRestore();
    });

    it('fait la première chose', () => {
        /* Test avec l'implémentation moquée par défaut */
    });

    it('fait la deuxième chose', () => {
        mockedFn.mockImplementation(() => {/*implémentation personnalisée juste pour ce test*/});
        /* Test en utilisant cette implémentation moquée personnalisée. Elle est réinitialisée après le test. */
    });

    it('fait la troisième chose', () => {
        /* Autre test avec l'implémentation moquée par défaut */
    });
});

1voto

Foxlab Points 131

Je n'ai pas réussi à définir le mock à l'intérieur du test lui-même, alors j'ai découvert que je pouvais simuler plusieurs résultats pour le même mock de service de cette manière :

jest.mock("@/services/ApiService", () => {
    return {
        apiService: {
            get: jest.fn()
                    .mockResolvedValueOnce({response: {value:"Value", label:"Test"}})
                    .mockResolvedValueOnce(null),
        }
    };
});

J'espère que cela aidera quelqu'un :)

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