Pour faciliter les explications, voici une fonction triviale, qu’on peut supposer être une fonction importante à mocker.
exports.bob = () => {
// fonction très TRÈS complexe...
return "real";
}
Problème: Les mocks ne se réinitialisent pas automatiquement après chaque test. Il faut le faire manuellement et il y a deux méthodes équivalentes.
const index = require("./index")
test("test 1", () => {
const original = index.bob;
index.bob = jest.fn(() => 0);
expect(index.bob(0)).toBe("mocked");
index.bob = original; // <-- on remet la fonction originale en place
});
Ou encore
test("test 1", () => {
const bob = jest.spyOn(index, "bob");
bob.mockImplementation(() => "mocked");
expect(index.bob(0)).toBe("mocked");
addMock.mockRestore(); // <-- on remet la fonction originale en place
});
Oui, ça fonctionne, le hic c’est que si un test plante avant de réinitialiser le mock, les autres tests qui dépendent de ladite fonction planteront. Par exemple:
test("test 1", () => {
original = index.bob;
index.bob = jest.fn(() => 0);
expect(index.bob(0)).toBe("mocked");
throw "erreur"; // oups!
index.bob = original; // ça n'arrive pas
});
test("test 2", () => {
expect(index.bob(7)).toBe("real"); // va échouer car bob retourne encore 0
})
Solution 1 – try / finally
test("test 1", () => {
original = index.bob;
try {
index.bob = jest.fn(() => "mocked");
expect(index.bob(10)).toBe("mocked");
throw "erreur"; // oups!
} finally {
index.bob = original;
}
});
test("test 2", () => {
expect(index.bob(7)).toBe("real");
})
C’est n’est pas tellement élégant car ça indente le code. C’est aussi facile d’oublier de mettre l’étape de réinitialisation dans un finally
.
Solution 2 – Une fonction router / proxy
const original_bob = index.bob;
index.bob = jest.fn((arg) => {
// un ou plusieurs if/else pour diriger les mocks
if (arg === 0) {
// mock pour "test 1"
return "mocked";
}
// si rien ne correspond, on appelle la fonction originale
return original_bob(arg);
});
test("test 1", () => {
expect(index.bob(0)).toBe("mocked");
});
test("test 2", () => {
expect(index.bob(10)).toBe("real");
})
Gros défaut: Ça ne fonctionne pas pour les fonctions qui n’ont pas d’argument. Autre inconvénient, la fonction peut rapidement devenir lourde s’il y a beaucoup de if
/ else
. Pour ne pas perdre le fil, ça demande d’écrire des commentaires pour garder une trace de l’association entre chaque mock et test. Si on renomme un test sans mettre à jour le commentaire dans le mock, le lien est rompu.
De l’autre côté, cette approche a l’avantage de pouvoir réutiliser les mocks entre les tests sans devoir les reconfigurer à chaque test. De plus, elle n’exige pas au programmeur de devoir se rappeler à chaque fois d’encapsuler le mock dans un bloc try
/ finally
et elle ne crée pas d’indentation du code.
Conclusion
Je théorise que la source du problème est probablement attribuable à l’absence de destructeurs en JavaScript, ce qui empêche d’encapsuler les mocks dans des objets et d’utiliser la technique resource acquisition is initialization, aussi connue sous RAII.
Merci JavaScript. Merci.