Les Test Doubles sont des objets substituts utilisés dans les tests unitaires à la place des dépendances réelles. Le terme a été introduit par Gerard Meszaros dans le livre « xUnit Test Patterns » (2007) comme un concept général pour Mock, Stub, Fake, Spy et Dummy. Selon Martin Fowler (2024), les Test Doubles permettent d'isoler le composant testé de son environnement, rendant les tests déterministes, rapides et indépendants des services externes.
Points clés
Test Doubles est un terme issu de l'industrie automobile (cascadeur), transposé dans le développement logiciel. Comme un cascadeur remplace un acteur dans une scène dangereuse, un Test Double remplace un composant réel dans un scénario de test. Cela est nécessaire lorsque la dépendance réelle est indisponible, lente, non déterministe ou a des effets secondaires.
Le concept de Test Double englobe cinq types spécifiques, chacun résolvant sa propre tâche. La typologie de Meszaros est canonique et utilisée dans tous les guides de test modernes. La différence entre les types réside dans le degré de contrôle et de vérification : du simple remplissage de paramètres (Dummy) à la vérification complète de la séquence d'appels (Mock).
Le but principal des Test Doubles est l'isolation du module testé. Dans le développement mobile, les dépendances réelles incluent les serveurs API, les bases de données, le système de fichiers, les capteurs de l'appareil et les services système (LocationManager, Camera, Bluetooth). L'utilisation directe de ces composants rend les tests lents, fragiles et dépendants de l'environnement. Selon Google Testing Blog (2023), les tests unitaires bien isolés s'exécutent en millisecondes, tandis que les tests d'intégration s'exécutent en secondes et minutes.
La classification de Gerard Meszaros comprend cinq types de Test Doubles, différant par le comportement et l'objectif. Comprendre la différence entre eux est la base de tests unitaires compétents.
Dummy est un objet passé à la méthode testée mais jamais utilisé. Dummy n'est nécessaire que pour satisfaire la signature de la méthode. En Kotlin, c'est souvent null, emptyList() ou un objet avec des stubs. Dummy ne doit contenir aucune logique — s'il est appelé, le test doit échouer.
Fake est une implémentation simplifiée mais fonctionnelle d'une interface. Contrairement à Mock et Stub, Fake contient une véritable logique métier, mais sous une forme simplifiée. Un exemple classique est InMemoryUserRepository, qui stocke les données dans une HashMap au lieu d'une base de données. Fake est utilisé lorsque vous devez tester une logique qui dépend de l'état, mais sans les frais généraux de l'infrastructure réelle.
| Type | Objectif | Exemple |
|---|---|---|
| Dummy | Remplir un paramètre | null, objet vide |
| Fake | Implémentation simplifiée fonctionnelle | InMemoryRepository |
| Stub | Retourner une valeur fixe | when(api.getUser()).thenReturn(user) |
| Spy | Enregistrer les appels pour vérification | verify(spy).save(user) |
| Mock | Vérifier l'interaction | verify(mock).sendEmail(email) |
Stub retourne des valeurs prédéfinies pour des appels spécifiques. Stub ne vérifie pas s'il a été appelé — il fournit simplement des données. Dans Mockito, Stub est créé via when(method).thenReturn(value). Stub est idéal pour les tests lorsque vous avez besoin qu'une dépendance retourne une valeur spécifique, mais le fait de l'appel lui-même n'est pas important.
Spy est une enveloppe autour d'un objet réel qui enregistre tous les appels pour vérification ultérieure. Contrairement à Mock, Spy délègue les appels à l'objet réel mais permet de vérifier qu'ils ont eu lieu. Dans Mockito, Spy est créé via spy(realObject). Spy est utile pour le mocking partiel, lorsque vous voulez utiliser un objet réel mais vérifier certains appels.
Mock est un objet avec des attentes d'appel prédéfinies. Mock vérifie que des méthodes spécifiques ont été appelées avec des arguments spécifiques et dans un ordre spécifique. Contrairement à Stub, Mock se concentre sur la vérification du comportement plutôt que sur le retour de données. Mock est le type de Test Double le plus puissant et le plus utilisé dans le développement mobile.
La différence entre Mock et Stub cause souvent de la confusion même chez les développeurs expérimentés. La différence principale réside dans l'objectif : Stub vérifie l'état (state verification), Mock vérifie le comportement (behavior verification).
Stub répond à la question : « le code a-t-il retourné le résultat correct ? ». Mock répond à la question : « le code a-t-il appelé les bonnes méthodes avec les bons arguments ? ». Dans le développement mobile, Stub est utilisé lorsque le résultat compte (ex. : données d'un repository), tandis que Mock est utilisé lorsque les effets secondaires comptent (ex. : envoi d'e-mail, écriture dans une base de données).
// Stub : vérification d'état
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock : vérification de comportement
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Exemples pratiques des cinq types de Test Doubles en Kotlin utilisant MockK — la bibliothèque de mocking la plus populaire pour les projets Android.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub : retourner une réponse API fixe
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify : vérifier que l'utilisateur a été sauvegardé
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy : Context n'est pas utilisé dans Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Le choix du type de Test Double dépend de ce qui est exactement testé : l'état, le comportement ou l'intégration. Dans le développement mobile Android et iOS, les recommandations suivantes ont été établies.
Lors du test de ViewModel, utilisez Mock pour les dépendances qui produisent des effets secondaires (repositories, analytics, navigation) et Stub pour les dépendances qui retournent des données (clients API, ContentProvider). Cela permet de vérifier que le ViewModel gère correctement les scénarios de succès et d'erreur.
Au niveau Repository, préférez Fake (implémentations de base de données en mémoire) et Stub (réponses API fixes). Fake permet de tester la logique de cache et le mode hors ligne sans configurer SQLite. Stub simule différents statuts HTTP : 200, 404, 500, timeout.
L'utilisation incorrecte des Test Doubles est l'une des causes les plus courantes de tests fragiles qui se brisent à chaque refactorisation.
L'erreur la plus courante est de moquer tout. Si chaque dépendance dans un test est remplacée par un Mock, le test cesse de vérifier le comportement réel. Mock ne doit être utilisé que pour les dépendances externes (réseau, base de données, système de fichiers, services système). Les composants internes de l'application (Value Object, data class, utilitaires simples) ne doivent pas être remplacés.
La deuxième erreur est de créer un Mock sans définir d'attentes. Si une méthode est appelée sans every / when, Mock retourne une valeur par défaut (null, 0, false). Cela peut conduire à des tests faux-positifs, où Mock retourne silencieusement null et le test interprète cela comme un comportement correct.
La troisième erreur est de vérifier chaque appel de chaque Mock. Verify ne doit être utilisé que pour les appels qui sont d'une importance critique du point de vue de la logique métier. La vérification excessive rend les tests fragiles : modifier l'ordre des appels dans le code de production brise les tests sans modifier le comportement.
Questions fréquentes
Stub retourne des données et vérifie l'état (ce qui a été retourné), tandis que Mock vérifie le comportement (quelles méthodes ont été appelées). Stub = « retourne X », Mock = « vérifie que Y a été appelé avec l'argument Z ». Dans les tests réels, un même objet agit souvent à la fois comme Stub et Mock.
Fake est préférable à Mock lorsque l'on teste une logique qui dépend de l'état : mise en cache, mode hors ligne, transactions. Fake (implémentation en mémoire) permet de tester ces scénarios sans appels verify fragiles. Mock est mieux adapté pour vérifier l'envoi de données : analytics, push, email.
Pour les projets Android en Kotlin, MockK est recommandé. Il prend en charge les coroutines, les fonctions suspend, les classes scellées et les fonctions d'extension sans configuration supplémentaire. Pour les projets Java, Mockito reste la norme — la bibliothèque la plus populaire avec une documentation complète.
Pour tester Kotlin Flow, utilisez la bibliothèque Turbine avec MockK. Turbine simplifie la vérification des émissions de Flow : vous pouvez vérifier l'ordre des valeurs, la fin du flux et les exceptions. Stub pour Flow retourne flowOf(value), Mock vérifie que le Flow a été collecté.
Oui, mais au niveau des réponses API, pas des composants UI. Les bibliothèques MockWebServer (OkHttp) et WireMock permettent de simuler des réponses HTTP dans les tests UI. Les composants UI eux-mêmes (Compose, SwiftUI Views) ne doivent pas être remplacés — leur comportement est testé via des tests de capture d'écran et Espresso.
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