Pour faciliter les explications, voici une fonction triviale, qu’on peut supposer être une fonction importante à mocker.
1 2 3 4 | 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.
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 | 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:
1 2 3 4 5 6 7 8 9 10 11 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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.