Excessive Mocking.
Un test met en place tant d'objets simulés (mocks) et d'interactions bouchonnées que la configuration des mocks éclipse la vérification réelle : le test finit par exercer les mocks plutôt que le comportement réel.
##Signs and Symptoms
Un test est surtout de l'arrange : de longs blocs de création de mocks, des lignes when(...).thenReturn(...) / mockReturnValue(...) et verify(...), avec peu de vrai code testé. Signes révélateurs :
- Plus de tuyauterie de mocks que d'assertions. La mise en place fait de nombreuses lignes ; l'« act » est un seul appel ; l'« assert » vérifie que les mocks ont été appelés plutôt qu'un résultat correct.
- Simuler ce qui vous appartient. Des objets de domaine, des objets-valeurs ou des collaborateurs de logique pure sont simulés au lieu de ne simuler que les frontières externes (réseau, base de données, horloge, système de fichiers, tiers).
- Chaînes de mocks / « train wrecks ». Un mock renvoie un autre mock qui renvoie un autre mock, reflétant le graphe d'appels de production.
- Vérification d'interaction seule. Le test vérifie
toHaveBeenCalledWith(...)sur chaque collaborateur et ne vérifie jamais la valeur renvoyée ni l'état résultant. - Fragile au refactoring. Réordonner des appels internes ou extraire une méthode casse de nombreux tests alors même que le comportement est inchangé.
- Cinq mocks ou plus rien que pour instancier le SUT — signe habituel que le SUT lui-même a trop de dépendances.
test('places order', () => {
const inventory = { check: jest.fn().mockReturnValue(true) };
const pricing = { quote: jest.fn().mockReturnValue(42) };
const tax = { calc: jest.fn().mockReturnValue(4.2) };
const wallet = { charge: jest.fn().mockReturnValue({ ok: true }) };
const ledger = { record: jest.fn() };
const emailer = { send: jest.fn() };
const audit = { log: jest.fn() };
const clock = { now: jest.fn().mockReturnValue(0) };
const svc = new OrderService(inventory, pricing, tax, wallet, ledger, emailer, audit, clock);
svc.place(cart);
// vérifie que les mocks ont été appelés — pas que la commande est correcte
expect(inventory.check).toHaveBeenCalled();
expect(pricing.quote).toHaveBeenCalled();
expect(wallet.charge).toHaveBeenCalledWith(46.2);
expect(ledger.record).toHaveBeenCalled();
});
##Reasons for the Problem
Pourquoi cela se produit
- Le SUT a trop de collaborateurs. Un excès de mocks est généralement un signal de conception : une classe à faible cohésion et aux nombreuses dépendances oblige chaque test à toutes les mettre en place. Comme le formule la littérature du catalogue : « si vous devez simuler cinq classes internes juste pour tester une méthode, la méthode a trop de dépendances — c'est un problème de conception, pas de test. »
- Habitude / réflexe du « tout simuler ». Pousser à l'extrême les tests fondés sur les interactions (« école de Londres »), ou simuler par réflexe pour éviter de toucher une base de données ou le réseau, conduit à simuler de la logique pure qui pourrait être testée directement.
- Objets réels difficiles à construire. Lorsque les vrais collaborateurs sont pénibles à instancier, un mock paraît plus simple que de corriger le constructeur ou d'ajouter un faux (fake).
Pourquoi c'est nuisible
- Fausse confiance. Les mocks encodent vos hypothèses sur une dépendance. Si l'implémentation réelle diverge, le test passe quand même — par ex. un bouchon qui prétend que
sum()ne renvoie que des entiers positifs maintient un test au vert après que la vraie méthode a changé. De tels tests peuvent devenir tautologiques : ils ne vérifient que le fait que les mocks que vous avez écrits se comportent comme les mocks que vous avez écrits. Comme l'avertit la documentation même de Mockito : « si tout est simulé, testons-nous vraiment le code de production ? » - Fragilité / forte maintenance. Vérifier des appels précis fait des détails d'implémentation le contrat : des refactorings anodins (ordre des appels, assistants extraits) cassent les tests. C'est l'Overspecified Software / le Fragile Test de Meszaros.
- Lisibilité médiocre. Des centaines de lignes de configuration de dépendances enterrent l'unique chose dont parle le test, à la manière d'un Mystery Guest — un relecteur ne peut pas dire quel comportement est réellement vérifié.
- Bogues d'intégration manqués. Le vrai câblage entre les composants n'est jamais exercé ; les bogues vivent précisément dans les jointures que les mocks ont remplacées. Une couverture élevée masque une faible qualité.
##Treatment
Traitez un mocking abondant comme un retour d'information, puis réduisez le besoin de simuler :
- Corrigez d'abord la conception. Si vous devez simuler 5 collaborateurs ou plus pour construire le SUT, répartissez les responsabilités ou réduisez les dépendances du constructeur. Moins de dépendances réelles signifie moins de mocks.
- Ne simulez qu'aux frontières d'architecture. Simulez ce que vous ne possédez pas (réseau, base de données, horloge, système de fichiers, API tierces) ; utilisez des instances réelles de vos propres objets de domaine et objets-valeurs.
- Extrayez un cœur pur (functional core / imperative shell). Sortez la logique de calcul/décision de la classe à forte charge d'E/S afin de pouvoir la tester avec zéro mock, ne laissant qu'une fine coquille qui n'a besoin que d'un ou deux doublures de frontière.
- Préférez la vérification d'état à la vérification d'interaction. Vérifiez la valeur renvoyée ou l'état résultant plutôt que
verify(...)/toHaveBeenCalledWith(...)sur chaque collaborateur. Réservez les vérifications d'interaction au seul effet de bord qui compte vraiment. - Utilisez la doublure la plus simple qui fonctionne. Remplacez les mocks élaborés par des bouchons (stubs) (qui se contentent de renvoyer des valeurs) ou par un unique faux en mémoire réutilisable au lieu de re-bouchonner chaque méthode à chaque test.
- Faites ressortir le sur-mocking dynamiquement. Activer le bouchonnage strict de Mockito (le
MockitoExtension/MockitoJUnitRunnerpar défaut) lève uneUnnecessaryStubbingExceptionpour les bouchons configurés mais jamais utilisés — un moyen peu coûteux de trouver les mocks dont vous n'aviez pas besoin.
// AVANT : 8 mocks, on vérifie les appels
const pricing = { quote: jest.fn().mockReturnValue(42) };
const tax = { calc: jest.fn().mockReturnValue(4.2) };
// + 6 mocks de plus ...
expect(wallet.charge).toHaveBeenCalledWith(46.2);
// APRÈS : logique pure testée directement — aucun mock
expect(totalFor(cart, rates)).toBe(46.2);
// seule la vraie frontière est simulée ; on vérifie l'état, pas les appels
const wallet = new InMemoryWallet({ balance: 100 });
const svc = new OrderService(new InMemoryInventory(cart), wallet);
const order = svc.place(cart);
expect(order.total).toBe(46.2);
expect(wallet.balance).toBe(53.8);