web-dev-qa-db-fra.com

Comment se moquer d'un appel de fonction asynchrone dans une autre classe

J'ai le composant suivant (simplifié) React.

class SalesView extends Component<{}, State> {
  state: State = {
    salesData: null
  };

  componentDidMount() {
    this.fetchSalesData();
  }

  render() {
    if (this.state.salesData) {
      return <SalesChart salesData={this.state.salesData} />;
    } else {
      return <p>Loading</p>;
    }
  }

  async fetchSalesData() {
    let data = await new SalesService().fetchSalesData();
    this.setState({ salesData: data });
  }
}

Lors du montage, je récupère les données d'une API, que j'ai extraites dans une classe appelée SalesService. Je veux me moquer de cette classe, et pour la méthode fetchSalesData je veux spécifier les données de retour (dans une promesse).

Voici à peu près à quoi je veux que mon scénario de test ressemble:

  • prédéfinir les données de test
  • importer SalesView
  • simuler SalesService
  • configurez mockSalesService pour renvoyer une promesse qui renvoie les données de test prédéfinies une fois résolues

  • créer le composant

  • attendre
  • vérifier l'instantané

Tester l'apparence de SalesChart ne fait pas partie de cette question, j'espère résoudre cela en utilisant Enzyme. J'ai essayé des dizaines de choses pour se moquer de cet appel asynchrone, mais je n'arrive pas à le faire correctement. J'ai trouvé les exemples suivants de moquerie Jest en ligne, mais ils ne semblent pas couvrir cette utilisation de base.

Mes questions sont:

  • À quoi devrait ressembler la classe fictive?
  • Où dois-je placer cette classe simulée?
  • Comment dois-je importer cette classe fictive?
  • Comment puis-je savoir que cette classe simulée remplace la classe réelle?
  • Comment configurer l'implémentation fictive d'une fonction spécifique de la classe fictive?
  • Comment attendre dans le cas de test que la promesse soit résolue?

Un exemple que j'ai qui ne fonctionne pas est donné ci-dessous. Le lanceur de test se bloque avec l'erreur throw err; Et la dernière ligne de la trace de la pile est at process._tickCallback (internal/process/next_tick.js:188:7)

# __tests__/SalesView-test.js
import React from 'react';
import SalesView from '../SalesView';

jest.mock('../SalesService');
const salesServiceMock = require('../SalesService').default;

const weekTestData = [];

test('SalesView shows chart after SalesService returns data', async () => {
  salesServiceMock.fetchSalesData.mockImplementation(() => {
    console.log('Mock is called');
    return new Promise((resolve) => {
      process.nextTick(() => resolve(weekTestData));
    });
  });

  const wrapper = await shallow(<SalesView/>);
  expect(wrapper).toMatchSnapshot();
});
21
physicalattraction

Parfois, lorsqu'un test est difficile à écrire, il essaie de nous dire que nous avons un problème de conception.

Je pense qu'un petit refactoriseur pourrait rendre les choses beaucoup plus faciles - faire de SalesService un collaborateur au lieu d'un interne.

J'entends par là, au lieu d'appeler new SalesService() à l'intérieur de votre composant, accepter le service de vente comme un accessoire par le code appelant. Si vous faites cela, alors le code appelant peut également être votre test, auquel cas tout ce que vous devez faire est de se moquer du SalesService lui-même, et de renvoyer tout ce que vous voulez (en utilisant sinon ou toute autre bibliothèque de simulation, ou même création d'un talon roulé à la main).

12
Kraylog

Vous pouvez potentiellement supprimer le mot clé new à l'aide d'une méthode SalesService.create(), puis utiliser jest.spyOn (object, methodName) pour simuler l'implémentation.

import SalesService from '../SalesService ';

test('SalesView shows chart after SalesService returns data', async () => {

    const mockSalesService = {
        fetchSalesData: jest.fn(() => {
            return new Promise((resolve) => {
                process.nextTick(() => resolve(weekTestData));
            });
        })
    };

    const spy = jest.spyOn(SalesService, 'create').mockImplementation(() => mockSalesService);

    const wrapper = await shallow(<SalesView />);
    expect(wrapper).toMatchSnapshot();
    expect(spy).toHaveBeenCalled();
    expect(mockSalesService.fetchSalesData).toHaveBeenCalled();

    spy.mockReset();
    spy.mockRestore();
});
5
Jake Holzinger

Une façon "laide" que j'ai utilisée dans le passé est de faire une sorte d'injection de dépendance du pauvre.

Il est basé sur le fait que vous ne voudrez peut-être pas vraiment instancier SalesService à chaque fois que vous en avez besoin, mais plutôt que vous souhaitez conserver une seule instance par application, que tout le monde utilise. Dans mon cas, SalesService a nécessité une configuration initiale que je ne voulais pas répéter à chaque fois. [1]

J'ai donc eu un fichier services.ts Qui ressemble à ceci:

/// In services.ts
let salesService: SalesService|null = null;
export function setSalesService(s: SalesService) {
    salesService = s;
}
export function getSalesService() {
    if(salesService == null) throw new Error('Bad stuff');
    return salesService;
}

Ensuite, dans index.tsx De mon application ou un autre endroit similaire, j'aurais:

/// In index.tsx
// initialize stuff
const salesService = new SalesService(/* initialization parameters */)
services.setSalesService(salesService);
// other initialization, including calls to React.render etc.

Dans les composants, vous pouvez alors simplement utiliser getSalesService pour obtenir une référence à l'instance unique SalesService par application.

Quand vient le temps de tester, il vous suffit de faire une configuration dans vos gestionnaires mocha (ou autre) before ou beforeEach pour appeler setSalesService avec un objet simulé.

Maintenant, idéalement, vous voudriez passer SalesService comme accessoire à votre composant, car il est une entrée pour lui, et en utilisant getSalesService vous cachez cette dépendance et vous causerez peut-être des ennuis en cours de route. Mais si vous en avez besoin dans un composant très imbriqué, ou si vous utilisez un routeur ou quelque chose du genre, il devient assez difficile de le passer comme accessoire.

Vous pouvez également vous en sortir en utilisant quelque chose comme context , pour tout garder à l'intérieur React pour ainsi dire.

La solution "idéale" pour cela serait quelque chose comme l'injection de dépendance, mais ce n'est pas une option avec React AFAIK.


[1] Il peut également aider à fournir un point unique pour la sérialisation des appels de service à distance, qui pourraient être nécessaires à un moment donné.

2
Horia Coman