Échecs du Comptage d'Appels Mock Après Ajout de Logique de Retry/Fallback
Problème
Lorsque vous ajoutez une logique de retry, des mécanismes de fallback ou des chemins de code alternatifs à un service, les tests existants qui vérifient le nombre exact d'appels aux méthodes mockées échouent. Les tests ont été écrits pour le comportement d'origine et ne tiennent pas compte des nouveaux appels de retry/fallback.
Contexte / Conditions Déclenchantes
- Le test échoue avec :
Expected: <N>, Actual: <M>où M > N - Message d'erreur : "Unexpected number of calls"
MissingStubError: 'methodName' No stub was found which matches the arguments- Vous avez récemment ajouté :
- Une logique de retry (p. ex., retry après timeout)
- Des mécanismes de fallback (p. ex., essayer le serveur principal, revenir au serveur secondaire)
- Des chemins de code alternatifs (p. ex., essayer l'API REST, revenir à WebSocket)
- Le test échouant utilise des assertions
verify(...).called(N)
Solution
Étape 1 : Identifier les Nouveaux Chemins de Code
Parcourez votre nouvelle logique de retry/fallback pour comprendre :
- Combien de fois la méthode mockée sera maintenant appelée
- Quelles nouvelles méthodes sont appelées qui ne l'étaient pas avant
Étape 2 : Mocker les Nouvelles Dépendances
Si votre code de fallback appelle des méthodes qui n'étaient pas précédemment mockées :
// AVANT : Seule la méthode principale était mockée
when(mockService.primaryMethod(any)).thenAnswer((_) async => 'result');
// APRÈS : Mocker aussi les méthodes liées au fallback
when(mockService.primaryMethod(any)).thenAnswer((_) async => 'result');
when(mockService.addFallbackServer(any)).thenAnswer((_) async => false); // Échec gracieux
when(mockService.queryFallback(any)).thenAnswer((_) async => []);
Étape 3 : Mettre à Jour les Comptages d'Appels Attendus
// AVANT : Appel unique attendu
verify(mockService.createSubscription(...)).called(1);
// APRÈS : Appels attendus = initial + retries
// Documenter POURQUOI le comptage a changé
verify(mockService.createSubscription(...)).called(2); // initial + retry après fallback
Étape 4 : Ajouter des Commentaires Expliquant le Comptage
// Vérifier que l'abonnement a été créé
// Note : Avec la logique de fallback de l'indexeur, createSubscription est appelée deux fois :
// 1. Première tentative via récupération de lot du relais principal
// 2. Tentative de retry après échec du fallback de l'indexeur
verify(
mockService.createSubscription(...),
).called(2);
Vérification
- Exécutez le test spécifique :
flutter test --name "test name" - Vérifiez que le test passe
- Vérifiez que le comportement réel du service correspond au comptage de retry attendu
Exemple
Scénario : Ajout d'un fallback de relais d'indexeur à la récupération de profil
Comportement d'origine :
- Récupérer le profil depuis le relais principal → 1 abonnement créé
Nouveau comportement :
- Récupérer le profil depuis le relais principal (abonnement #1)
- Si non trouvé, essayer les relais d'indexeur
- Si les indexeurs échouent, réessayer le relais principal (abonnement #2)
- Après 2 échecs, marquer le profil comme manquant
Correction du test :
test('should force refresh profile with forceRefresh parameter', () async {
// Configurer le gestionnaire d'abonnement mocké
when(
mockSubscriptionManager.createSubscription(...),
).thenAnswer((_) async => 'sub_123');
// Mocker addRelay pour retourner false (relais d'indexeur non disponibles)
// Cela prévient MissingStubError et simule des indexeurs indisponibles
when(mockNostrService.addRelay(any)).thenAnswer((_) async => false);
// ... configuration du test ...
await service.fetchProfile(pubkey, forceRefresh: true);
// Attendre 2 appels : tentative initiale + retry après fallback de l'indexeur
verify(
mockSubscriptionManager.createSubscription(...),
).called(2);
});
Notes
- Ne changez pas juste le nombre : Comprenez toujours POURQUOI le comptage a changé
- Mocker les chemins de fallback pour qu'ils échouent gracieusement : Retourner false/empty au lieu de lever une exception
- Considérer l'isolation des tests : Certains tests peuvent avoir besoin de configurations de mock différentes pour des scénarios différents
- Documenter les changements de comportement : Les futurs mainteneurs doivent comprendre le flux prévu
- Ce pattern s'applique universellement : Java Mockito, Python unittest.mock, Jest, etc.
Patterns Associés
- Lors de l'ajout de mise en cache : les méthodes peuvent être appelées 0 fois en cas de cache hit
- Lors de l'ajout de circuit breakers : les méthodes peuvent être appelées moins de fois en cas de circuit ouvert
- Lors de l'ajout de rate limiting : les méthodes peuvent être retardées ou regroupées