Spy (espion) — un objet de test qui enveloppe une instance réelle et enregistre les informations sur chaque appel : quelles méthodes ont été appelées, avec quels arguments et combien de fois. Contrairement à un mock, un spy utilise l’implémentation réelle de l’objet enveloppé — les appels passent par le code réel, et le spy ne fait qu’enregistrer les faits. Après l’exécution du test, le développeur vérifie les enregistrements de l’espion : « La méthode sendAnalytics a-t-elle été appelée trois fois ? ». Pour en savoir plus, consultez le guide de test Android.
Points clés
Spy — une enveloppe autour d’un objet réel qui intercepte tous les appels de méthodes et les enregistre. La logique réelle de l’objet est exécutée : si la méthode enregistre des données, calcule une valeur ou effectue une requête — tout se passe normalement. De plus, le spy enregistre les métadonnées : nom de la méthode, arguments, nombre d’appels, temps d’exécution. Le terme fait partie de la classification de Meszaros (2007) et est décrit en détail dans l’article de Martin Fowler « Mocks Aren’t Stubs ».
Différence clé avec un Mock — un mock remplace complètement l’objet par un stub de test ; toutes les méthodes ne font rien par défaut. Un spy enveloppe un objet existant : toutes les méthodes fonctionnent normalement par défaut, mais sont également enregistrées. Cette distinction est fondamentale : un mock isole le code testé de la réalité, tandis qu’un spy préserve la réalité et permet de l’observer. Le choix entre eux dépend de ce qui est testé.
Un spy est le bon choix — si le code testé modifie l’état d’un objet réel, et que le test doit à la fois vérifier le résultat (état) et s’assurer que les appels ont été effectués dans le bon ordre. Un mock n’est pas adapté car il n’exécute pas l’implémentation réelle. Un stub n’est pas adapté car il n’enregistre pas les appels. Un spy est le seul double de test qui préserve à la fois la logique réelle et fournit des informations sur les appels.
Mock — isolation complète. Si le test ne doit pas dépendre de l’implémentation de l’objet réel (par exemple, une base de données ou un client réseau), utilisez un mock. Un mock garantit qu’aucun appel n’atteint le composant réel. C’est sûr et prévisible. L’inconvénient : un mock n’exécute pas de logique réelle, donc si le code testé dépend d’une valeur de retour, elle doit être configurée explicitement via when/stub.
Spy — logique réelle + observation. Si le code testé interagit avec un objet dont la logique est importante pour le test, et pas seulement les données — utilisez un spy. Par exemple, un AnalyticsTracker qui collecte des événements et les envoie périodiquement. Le test vérifie que les événements ont été ajoutés au tampon et qu’après l’envoi, le tampon a été vidé. Un mock ne peut pas vérifier cela car il n’exécute pas la logique réelle du tracker.
| Scénario | Spy | Mock |
|---|---|---|
| Logique réelle nécessaire | Oui | Non (stub) |
| Vérification d’appels | Oui (nombre, arguments) | Oui (nombre, arguments) |
| Stubbing partiel | Oui (certaines méthodes spy, d’autres stub) | Non (toutes les méthodes sont des stubs) |
| Risque d’effets secondaires | Élevé (code réel) | Nul |
| Vitesse | Inférieure (logique réelle) | Supérieure (stubs) |
| Lisibilité | Inférieure (plus difficile de comprendre ce qui est réel) | Supérieure (tout est explicite) |
Spy pour tout — utiliser un spy au lieu d’un mock pour tous les tests est une erreur. Un spy exécute du code réel qui peut avoir des effets secondaires : écrire dans un fichier, envoyer HTTP, modifier l’état global. Si le module testé appelle une méthode d’un objet spy qui effectue une requête HTTP, le test devient un test d’intégration, pas un test unitaire. Règle : si un spy enveloppe un objet avec des opérations d’E/S — ce n’est plus un test unitaire. Utilisez des mocks pour isoler les E/S et des spies uniquement pour les objets en mémoire sans effets externes.
Mockito.spy() — la façon classique de créer un espion dans les projets Java/Kotlin. spy() prend un objet réel et retourne une enveloppe. Tous les appels sont délégués à l’objet réel par défaut et les résultats sont enregistrés. Après le test, vous pouvez utiliser verify() pour vérifier le nombre d’appels et les arguments. Pour les méthodes qui doivent retourner des données de test, on utilise doReturn/when — cela s’appelle « partial mocking ».
class AnalyticsReporterTest {
private val realTracker = AnalyticsTracker()
private val spyTracker = Mockito.spy(realTracker)
fun test_event_tracked() {
val event = AnalyticsEvent("login")
spyTracker.track(event)
Mockito.verify(spyTracker).track(event)
assertEquals(1, spyTracker.getBufferedCount())
}
fun test_track_with_exception() {
Mockito.doThrow(RuntimeException("network"))
.when(spyTracker).flush()
spyTracker.track(AnalyticsEvent("login"))
assertTrue(spyTracker.hasPendingEvents())
}
}
MockK.spyk() — une alternative pour les projets Kotlin avec un meilleur support des coroutines et des classes sealed. MockK.spyk() crée un espion, analogue à Mockito.spy(). Il prend en charge coVerify pour les fonctions suspend et every pour le stubbing partiel. Contrairement à Mockito, MockK ne prend pas en charge les spies pour les classes final (toutes les classes en Kotlin sont final par défaut) — vous devez ouvrir la classe (open) ou utiliser une interface.
class LoginUseCaseTest {
private val realRepo = UserRepository()
private val spyRepo = spyk(realRepo)
private val useCase = LoginUseCase(spyRepo)
fun test_login_calls_save() = runTest {
every { spyRepo.getUser(any()) } returns User("test")
val result = useCase.login("test", "pass")
coVerify { spyRepo.saveLoginTime(any()) }
assertTrue(result.isSuccess)
}
}
Stubbing partiel — une technique puissante mais dangereuse. Vous pouvez créer un spy d’un objet et ne remplacer (stub) que certaines méthodes, laissant les autres réelles. Exemple : un dépôt spy où getUser() retourne des données de test, tandis que saveUser() enregistre réellement dans une liste en mémoire. Cela permet de combiner les avantages des stubs (données contrôlées) et des spies (logique réelle). L’inconvénient : la lisibilité du test en pâtit — il n’est pas évident de savoir quelles méthodes sont réelles et lesquelles sont des stubs.
OCMock pour Objective-C — une bibliothèque qui prend en charge la création d’objets spy via niceMock. OCMock intercepte les appels de méthodes en utilisant l’environnement d’exécution Objective-C et les enregistre. Après le test, verify est appelé. OCMock prend en charge les spies pour n’importe quel objet (toutes les méthodes en Objective-C sont dynamiques), ce qui lui donne un avantage sur Swift, où les spies ne sont possibles que via des protocoles.
// Création d’un spy pour un objet réel
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Exécution du test
[spy trackEvent:@"login"];
// Vérification
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Spy basé sur protocole en Swift — Swift n’a pas de réflexion d’exécution comme Objective-C, donc les spies sont créés manuellement. Une structure de test implémente un protocole et appelle intérieurement l’objet réel tout en enregistrant simultanément les appels. Cela nécessite plus de code, mais est totalement contrôlable et type-safe. Les spies manuels ne nécessitent pas de bibliothèques externes et n’utilisent pas l’exécution — tout est vérifié à la compilation.
protocol AnalyticsProtocol {
func trackEvent(name: String)
}
final class SpyAnalytics: AnalyticsProtocol {
private let real: AnalyticsProtocol
private var events: [String] = []
init(real: AnalyticsProtocol) {
self.real = real
}
func trackEvent(name: String) {
events.append(name)
real.trackEvent(name: name)
}
func verifyTracked(name: String) -> Bool {
return events.contains(name)
}
}
Quand utiliser OCMock vs spy manuel — pour le code Objective-C, utilisez OCMock (moins de code passe-partout). Pour Swift, les spies manuels via des protocoles sont préférables. Un spy manuel donne un contrôle total sur l’enregistrement des appels, ne nécessite pas de réflexion et fonctionne avec les types valeur (structs). Le seul inconvénient est que vous devez maintenir le code de la classe spy synchronisé avec le protocole lors de l’ajout de nouvelles méthodes.
Vérification d’analytique — le cas d’utilisation le plus courant pour les spies. Dans le code de production, les appels d’analytique sont dispersés dans toute l’application : connexion, déconnexion, achat, erreur. Le test crée une enveloppe spy pour AnalyticsTracker, exécute un scénario (connexion, voir produit, ajouter au panier, acheter) et vérifie que tous les événements nécessaires ont été envoyés dans le bon ordre. Un mock n’est pas adapté car AnalyticsTracker contient une logique de mise en tampon et d’envoi.
Minuteries et planificateurs — tester le code qui utilise Handler (Android) ou Timer (iOS) est difficile à cause du temps réel. Une enveloppe spy pour Scheduler enregistre quelles tâches ont été planifiées et avec quel retard. Le test crée un spy du Handler réel, effectue une action et vérifie que Handler.postDelayed(runnable, delay) a été appelé avec le bon retard. La tâche réelle n’est pas exécutée — le spy intercepte et enregistre l’appel.
Journalisation et informations de débogage — en production, les journaux peuvent être désactivés ou écrits dans un fichier. Une enveloppe spy pour Logger enregistre tous les messages dans une liste en mémoire que le teste vérifie après l’exécution. Cela permet de vérifier que le message correct a été écrit en cas d’erreur sans encombrer la console. Les spies manuels pour Logger sont particulièrement utiles sur iOS, où OSLog n’a pas d’API de test.
Vérification de l’ordre des appels — certains scénarios nécessitent un ordre strict des opérations : ouvrir la connexion, envoyer des données, fermer la connexion. Mockito permet de vérifier l’ordre via InOrder.verify(). Un spy fait la même chose mais préserve l’exécution réelle. Si l’ordre et le résultat de chaque étape (la connexion s’est réellement ouverte) sont importants — utilisez un spy, pas un mock.
Foire aux questions
Spy enveloppe un objet réel et exécute sa logique, tout en enregistrant les appels. Mock remplace complètement l’objet par un stub — aucune logique réelle n’est exécutée. Un spy préserve le comportement, un mock non. Choisissez un spy lorsque le travail réel de l’objet est important ; choisissez un mock lorsque vous devez isoler le test d’une dépendance externe.
Lorsqu’une enveloppe spy entraîne de véritables opérations d’E/S. Si un spy enveloppe un objet qui écrit dans un fichier, envoie HTTP ou lit depuis le disque — le test n’est plus unitaire. Deuxième cas : le test vérifie uniquement la valeur de retour sans s’intéresser aux appels — ici un stub suffit et un spy est redondant. Troisième : le code dépend de l’état interne du spy — c’est un test fragile.
Oui, via spyk() — l’équivalent de Mockito.spy(). MockK.spyk() crée un espion autour d’un objet réel, supporte every pour le stubbing partiel et coVerify/coroutinesVerify pour les fonctions suspend. Limitation : ne fonctionne pas avec les classes final (nécessite open ou interface). Pour les classes Java, MockK supporte également spyk() mais nécessite l’annotation @MockKJvmInline.
Techniquement, non. Mock est un stub qui ne contient pas d’implémentation réelle. Spy, par définition, enveloppe un objet réel. Dans Mockito, vous ne pouvez pas transformer un mock en spy. Mais vous pouvez faire l’inverse : créer un spy et remplacer certaines méthodes via doReturn/when (partial mocking). Cela donne un comportement semblable à un mock pour les méthodes sélectionnées de l’objet spy.
Oui, c’est nécessaire. En Swift, il n’y a pas de proxy dynamique comme en Java/Kotlin. Pour créer un spy, vous avez besoin d’un protocole que la classe de production et la classe spy implémentent toutes deux. Un spy basé sur protocole en Swift est une implémentation manuelle qui prend un objet réel, lui délègue les appels et enregistre les métadonnées. Alternative : la bibliothèque Cuckoo, qui génère des classes spy via SourceKit.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi