mock-call-count-retry-fallback

Par divinevideo · divine-mobile

Corriger les échecs de tests lorsque l'ajout d'une logique de retry/fallback provoque des incohérences dans le nombre d'appels aux mocks. À utiliser quand : (1) un test échoue avec « Expected: <1>, Actual: <2> » ou une erreur similaire de comptage d'appels après l'ajout d'un mécanisme de retry ou de fallback, (2) une `MissingStubError` apparaît pour des méthodes dans les nouveaux chemins de code fallback, (3) les assertions `verify().called(N)` de Mockito échouent après des changements de comportement. S'applique à Dart/Flutter avec Mockito, mais le pattern est universel.

npx skills add https://github.com/divinevideo/divine-mobile --skill mock-call-count-retry-fallback

É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

  1. Exécutez le test spécifique : flutter test --name "test name"
  2. Vérifiez que le test passe
  3. 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 :

  1. Récupérer le profil depuis le relais principal (abonnement #1)
  2. Si non trouvé, essayer les relais d'indexeur
  3. Si les indexeurs échouent, réessayer le relais principal (abonnement #2)
  4. 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

Références

Skills similaires