Parfois, vous souhaitez vous moquer d'une fonction dans certains tests mais pas dans d'autres. Parfois, vous souhaitez fournir différentes simulations à différents tests. Jest rend cela délicat : son comportement par défaut est de remplacer la fonction d'un package pour un fichier de test entier, pas seulement pour un seul test. Cela semble étrange si vous avez utilisé des outils flexibles comme le @patch de Python ou le conteneur de services de Laravel.
Cet article vous montrera comment simuler des fonctions pour des tests individuels, puis revenir à l'implémentation d'origine si aucune simulation n'a été fournie. Des exemples seront donnés pour les modules CommonJS et ES. Les techniques démontrées dans cet article fonctionneront à la fois pour les modules propriétaires et les packages tiers.
Étant donné que nous aborderons plusieurs systèmes de modules dans cet article, il est important de comprendre de quoi il s'agit.
CommonJS (en abrégé CJS) est le système de modules dans Node.js. Il exporte des fonctions à l'aide de module.exports et importe des fonctions à l'aide de require() :
// CommonJS export function greet() { return "Hello, world!"; } module.exports = { greet };
// CommonJS import const getUsersList = require('./greet');
Modules ES (en abrégé ESM) sont le système de modules utilisé par le navigateur. Il exporte des fonctions à l'aide du mot-clé export et importe des fonctions à l'aide du mot-clé import :
// ES module export export default function greet() { return "Hello, world!"; }
// ES module import import { greet } from "./greet";
La plupart des développeurs JavaScript frontend utilisent des modules ES au moment de la rédaction de cet article, et de nombreux développeurs JS côté serveur les utilisent également. Cependant, CommonJS est toujours la valeur par défaut pour Node. Quel que soit le système que vous utilisez, il vaut la peine de lire l'intégralité de l'article pour en savoir plus sur le système moqueur de Jest.
En général, un fichier CommonJS exportera ses modules en utilisant la syntaxe objet, comme indiqué ci-dessous :
// CommonJS export function greet() { return "Hello, world!"; } module.exports = { greet: greet };
Cependant, il est également possible d'exporter une fonction seule :
// CommonJS export function greet() { return "Hello, world!"; } module.exports = greet;
Je ne recommanderais pas nécessairement de faire cela dans votre propre code : exporter un objet vous donnera moins de maux de tête lors du développement de votre application. Cependant, c'est assez courant pour qu'il vaut la peine de discuter de la façon de simuler une simple fonction exportée dans CommonJS, puis de revenir à l'original si un test ne fournit pas sa propre implémentation.
Disons que nous avons le fichier CommonJS suivant dont nous aimerions nous moquer lors des tests :
// cjsFunction.js function testFunc() { return "original"; } module.exports = testFunc;
Nous pourrions nous en moquer dans nos tests en utilisant le code suivant :
const testFunc = require("./cjsFunction"); jest.mock("./cjsFunction"); beforeEach(() => { testFunc.mockImplementation(jest.requireActual("./cjsFunction")); }); it("can override the implementation for a single test", () => { testFunc.mockImplementation(() => "mock implementation"); expect(testFunc()).toBe("mock implementation"); expect(testFunc.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { testFunc.mockReturnValue("mock return value"); expect(testFunc()).toBe("mock return value"); expect(testFunc.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(testFunc()).toBe("original"); expect(testFunc.mock.calls).toHaveLength(1); });
Lorsque nous appelons jest.mock("./cjsFunction"), cela remplace le module (le fichier et toutes ses exportations) par un auto-mock (docs). Lorsqu'une simulation automatique est appelée, elle renvoie un élément indéfini. Cependant, il fournira des méthodes pour remplacer l'implémentation de la simulation, la valeur de retour, etc. Vous pouvez voir toutes les propriétés et méthodes qu'il fournit dans la documentation Jest Mock Functions.
Nous pouvons utiliser la méthode mockImplementation() du mock pour définir automatiquement l'implémentation du mock sur l'implémentation du module d'origine. Jest fournit une méthode jest.requireActual() qui chargera toujours le module d'origine, même s'il est actuellement simulé.
Les implémentations simulées et les valeurs de retour sont automatiquement effacées après chaque test, nous pouvons donc transmettre une fonction de rappel à la fonction beforeEach() de Jest qui définit l'implémentation de la simulation sur l'implémentation d'origine avant chaque test. Ensuite, tous les tests qui souhaitent fournir leur propre valeur de retour ou implémentation peuvent le faire manuellement dans le corps du test.
Disons que le code ci-dessus avait exporté un objet au lieu d'une seule fonction :
// cjsModule.js function testFunc() { return "original"; } module.exports = { testFunc: testFunc, };
Nos tests ressembleraient alors à ceci :
const cjsModule = require("./cjsModule"); afterEach(() => { jest.restoreAllMocks(); }); it("can override the implementation for a single test", () => { jest .spyOn(cjsModule, "testFunc") .mockImplementation(() => "mock implementation"); expect(cjsModule.testFunc()).toBe("mock implementation"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { jest.spyOn(cjsModule, "testFunc").mockReturnValue("mock return value"); expect(cjsModule.testFunc()).toBe("mock return value"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(cjsModule.testFunc()).toBe("original"); }); it("can spy on calls while keeping the original implementation", () => { jest.spyOn(cjsModule, "testFunc"); expect(cjsModule.testFunc()).toBe("original"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); });
La méthode jest.spyOn() permet à Jest d'enregistrer les appels à une méthode sur un objet et de fournir son propre remplacement. Cela uniquement fonctionne sur les objets, et nous pouvons l'utiliser car notre module exporte un objet qui contient notre fonction.
La méthode spyOn() est une simulation, son état doit donc être réinitialisé. La documentation de Jest spyOn() recommande de réinitialiser l'état en utilisant jest.restoreAllMocks() dans un rappel afterEach(), ce que nous avons fait ci-dessus. Si nous ne le faisions pas, la simulation renverrait undéfini lors du prochain test après l'appel de spyOn().
Les modules ES peuvent avoir des exportations par défaut et nommées :
// esmModule.js export default function () { return "original default"; } export function named() { return "original named"; }
Voici à quoi ressembleraient les tests pour le fichier ci-dessus :
import * as esmModule from "./esmModule"; afterEach(() => { jest.restoreAllMocks(); }); it("can override the implementation for a single test", () => { jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default"); jest .spyOn(esmModule, "named") .mockImplementation(() => "mock implementation named"); expect(esmModule.default()).toBe("mock implementation default"); expect(esmModule.named()).toBe("mock implementation named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { jest.spyOn(esmModule, "default").mockReturnValue("mock return value default"); jest.spyOn(esmModule, "named").mockReturnValue("mock return value named"); expect(esmModule.default()).toBe("mock return value default"); expect(esmModule.named()).toBe("mock return value named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(esmModule.default()).toBe("original default"); expect(esmModule.named()).toBe("original named"); });
Cela ressemble presque à l'exemple CommonJS précédent, avec quelques différences clés.
Tout d'abord, nous importons notre module en tant qu'importation d'espace de noms.
import * as esmModule from "./esmModule";
Puis quand on veut espionner l'export par défaut, on utilise "default":
jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default");
Sometimes when trying to call jest.spyOn() with a third-party package, you'll get an error like the one below:
TypeError: Cannot redefine property: useNavigate at Function.defineProperty (<anonymous>)
When you run into this error, you'll need to mock the package that is causing the issue:
import * as reactRouterDOM from "react-router-dom"; // ADD THIS: jest.mock("react-router-dom", () => { const originalModule = jest.requireActual("react-router-dom"); return { __esModule: true, ...originalModule, }; }); afterEach(() => { jest.restoreAllMocks(); });
This code replaces the module with a Jest ES Module mock that contains all of the module's original properties using jest.mocks's factory parameter. The __esModule property is required whenever using a factory parameter in jest.mock to mock an ES module (docs).
If you wanted, you could also replace an individual function in the factory parameter. For example, React Router will throw an error if a consumer calls useNavigate() outside of a Router context, so we could use jest.mock() to replace that function throughout the whole test file if we desired:
jest.mock("react-router-dom", () => { const originalModule = jest.requireActual("react-router-dom"); return { __esModule: true, ...originalModule, // Dummy that does nothing. useNavigate() { return function navigate(_location) { return; }; }, }; });
I hope this information is valuable as you write your own tests. Not every app will benefit from being able to fallback to the default implementation when no implementation is provided in a test itself. Indeed, many apps will want to use the same mock for a whole testing file. However, the techniques shown in this post will give you fine-grained control over your mocking.
Let me know if I missed something or if there's something that I didn't include in this post that should be here.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!