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:
configurez mockSalesService pour renvoyer une promesse qui renvoie les données de test prédéfinies une fois résolues
créer le composant
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:
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();
});
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).
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();
});
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é.