web-dev-qa-db-fra.com

jest.mock (): Comment se moquer de l'importation par défaut de la classe ES6 à l'aide du paramètre d'usine

Importations de classe ES6 moqueuses

Je voudrais me moquer de mes importations de classe ES6 dans mes fichiers de test.

Si la classe en cours de simulation a plusieurs consommateurs, il peut être judicieux de déplacer la simulation dans __mocks__, afin que tous les tests puissent partager la simulation, mais jusque-là, je voudrais conserver la simulation dans le fichier de test.

Jest.mock ()

jest.mock() peut se moquer des modules importés. Lorsque passé un seul argument:

jest.mock('./my-class.js');

il utilise l'implémentation fictive trouvée dans le dossier __mocks__ adjacent au fichier simulé, ou crée une simulation automatique.

Le paramètre d'usine du module

jest.mock() prend un second argument qui est une fabrique de modules une fonction. Pour les classes ES6 exportées à l'aide de export default, Ce que cette fonction d'usine doit renvoyer n'est pas clair. Est il:

  1. Une autre fonction qui renvoie un objet qui imite une instance de la classe?
  2. Un objet qui imite une instance de la classe?
  3. Un objet avec une propriété default qui est une fonction qui retourne un objet qui imite une instance de la classe?
  4. Une fonction qui renvoie une fonction d'ordre supérieur qui renvoie elle-même 1, 2 ou 3?

Les documents sont assez vagues:

Le deuxième argument peut être utilisé pour spécifier une fabrique de modules explicite qui est en cours d'exécution au lieu d'utiliser la fonction d'automockage de Jest:

J'ai du mal à trouver une définition d'usine qui puisse fonctionner comme un constructeur lorsque le consommateur imports la classe. Je reçois toujours TypeError: _soundPlayer2.default is not a constructor (Par exemple).

J'ai essayé d'éviter d'utiliser les fonctions fléchées (car elles ne peuvent pas être appelées avec new) et d'avoir la fabrique renvoyer un objet qui a une propriété default (ou pas).

Voici un exemple. Cela ne fonctionne pas; tous les tests lancent TypeError: _soundPlayer2.default is not a constructor.

Classe testée: sound-player-consumer.js

import SoundPlayer from './sound-player'; // Default import

export default class SoundPlayerConsumer {
  constructor() {
    this.soundPlayer = new SoundPlayer(); //TypeError: _soundPlayer2.default is not a constructor
  }

  playSomethingCool() {
    const coolSoundFileName = 'song.mp3';
    this.soundPlayer.playSoundFile(coolSoundFileName);
  }
}

Classe dont on se moque: sound-player.js

export default class SoundPlayer {
  constructor() {
    // Stub
    this.whatever = 'whatever';
  }

  playSoundFile(fileName) {
    // Stub
    console.log('Playing sound file ' + fileName);
  }
}

Le fichier de test: sound-player-consumer.test.js

import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';

// What can I pass as the second arg here that will 
// allow all of the tests below to pass?
jest.mock('./sound-player', function() { 
  return {
    default: function() {
      return {
        playSoundFile: jest.fn()
      };
    }
  };
});

it('The consumer should be able to call new() on SoundPlayer', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(soundPlayerConsumer).toBeTruthy(); // Constructor ran with no errors
});

it('We can check if the consumer called the mocked class constructor', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalled();
});

it('We can check if the consumer called a method on the class instance', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();
  expect(SoundPlayer.playSoundFile).toHaveBeenCalledWith(coolSoundFileName);
});

Que puis-je passer comme deuxième argument à jest.mock () qui permettra à tous les tests de l'exemple de réussir? Si les tests doivent être modifiés, ça va - tant qu'ils testent toujours les mêmes choses.

17
stone

Mise à jour avec une solution grâce à retour de @SimenB sur GitHub.


La fonction d'usine doit renvoyer une fonction

La fonction d'usine doit renvoyer la maquette: l'objet qui prend la place de tout ce qui se moque.

Puisque nous nous moquons d'une classe ES6, qui est ne fonction avec du sucre syntaxique , alors la maquette doit elle-même être une fonction. Par conséquent, la fonction d'usine passée à jest.mock() doit renvoyer une fonction; en d'autres termes, il doit s'agir d'une fonction d'ordre supérieur.

Dans le code ci-dessus, la fonction d'usine renvoie un objet. Étant donné que l'appel de new sur l'objet échoue, cela ne fonctionne pas.

Simulation simple que vous pouvez appeler new sur:

Voici une version simple qui, car elle retourne une fonction, permettra d'appeler new:

jest.mock('./sound-player', () => {
  return function() {
    return { playSoundFile: () => {} };
  };
});

Remarque: les fonctions fléchées ne fonctionneront pas

Notez que notre maquette ne peut pas être une fonction de flèche car nous ne pouvons pas appeler new sur une fonction de flèche en Javascript; c'est inhérent à la langue. Donc ça ne marchera pas:

jest.mock('./sound-player', () => {
  return () => { // Does not work; arrow functions can't be called with new
    return { playSoundFile: () => {} };
  };
});

Cela lancera TypeError: _soundPlayer2.default n'est pas un constructeur.

Garder une trace de l'utilisation (espionner la maquette)

Ne pas lancer d'erreurs est bien beau, mais nous devrons peut-être tester si notre constructeur a été appelé avec les paramètres corrects.

Afin de suivre les appels au constructeur, nous pouvons remplacer la fonction retournée par le HOF par une fonction de simulation Jest. Nous le créons avec jest.fn() , puis nous spécifions son implémentation avec mockImplementation() .

jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
    return { playSoundFile: () => {} };
  });
});

Cela nous permettra d'inspecter l'utilisation de notre classe simulée, en utilisant SoundPlayer.mock.calls.

Espionner les méthodes de notre classe

Notre classe simulée devra fournir toutes les fonctions membres (playSoundFile dans l'exemple) qui seront appelées lors de nos tests, sinon nous obtiendrons une erreur pour appeler une fonction qui n'existe pas. Mais nous voudrons probablement espionner également les appels à ces méthodes, pour nous assurer qu'ils ont été appelés avec les paramètres attendus.

Puisqu'un nouvel objet factice sera créé lors de nos tests, SoundPlayer.playSoundFile.calls Ne nous aidera pas. Pour contourner ce problème, nous remplissons playSoundFile avec une autre fonction de simulation et stockons une référence à cette même fonction de simulation dans notre fichier de test, afin que nous puissions y accéder pendant les tests.

let mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => { // Works and lets you check for constructor calls
    return { playSoundFile: mockPlaySoundFile }; // Now we can track calls to playSoundFile
  });
});

Exemple complet

Voici à quoi cela ressemble dans le fichier de test:

import SoundPlayerConsumer from './sound-player-consumer';
import SoundPlayer from './sound-player';

let mockPlaySoundFile = jest.fn();
jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: mockPlaySoundFile };
  });
});

it('The consumer should be able to call new() on SoundPlayer', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(soundPlayerConsumer).toBeTruthy(); // Constructor ran with no errors
});

it('We can check if the consumer called the class constructor', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  expect(SoundPlayer).toHaveBeenCalled();
});

it('We can check if the consumer called a method on the class instance', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  const coolSoundFileName = 'song.mp3';
  soundPlayerConsumer.playSomethingCool();
  expect(mockPlaySoundFile.mock.calls[0][0]).toEqual(coolSoundFileName);
});
18
stone

Si vous obtenez toujours TypeError: ...default is not a constructor Et utilisez TypeScript, continuez à lire.

TypeScript transpile votre fichier ts et votre module est probablement importé à l'aide de l'importation ES2015. const soundPlayer = require('./sound-player'). Par conséquent, la création d'une instance de la classe qui a été exportée par défaut ressemblera à ceci: new soundPlayer.default(). Cependant, si vous vous moquez de la classe comme suggéré par la documentation.

jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => {
    return { playSoundFile: mockPlaySoundFile };
  });
});

Vous obtiendrez la même erreur car soundPlayer.default Ne pointe pas vers une fonction. Votre maquette doit renvoyer un objet qui a une propriété par défaut qui pointe vers une fonction.

jest.mock('./sound-player', () => {
    return {
        default: jest.fn().mockImplementation(() => {
            return {
                playSoundFile: mockPlaySoundFile 
            }   
        })
    }
})
3
Santiago Ordonez

Pour tous ceux qui lisent cette question, j'ai configuré un référentiel GitHub pour tester les modules et les classes moqueurs. Il est basé sur les principes décrits dans la réponse ci-dessus, mais il couvre à la fois les exportations par défaut et les exportations nommées.

1
nidkil