Utiliser des doubles de test sur Android

Lors de la conception de la stratégie de test d'un élément ou d'un système, trois aspects sont associés:

  • Portée: quelle partie du code le test porte-t-il ? Les tests peuvent vérifier une seule méthode, l'ensemble de l'application ou quelque part entre les deux. Le champ d'application testé est en cours de test et fait généralement référence à l'objet du test, mais également au système en cours de test ou à l'unité en cours de test.
  • Vitesse: à quelle vitesse le test se déroule-t-il ? Les vitesses de test peuvent varier de l'ordre de quelques millisecondes à plusieurs minutes.
  • Fidélité: dans quelle mesure le test est-il "dans le monde réel" ? Par exemple, si une partie du code que vous testez doit envoyer une requête réseau, le code de test effectue-t-il réellement cette requête réseau ou simule-t-il le résultat ? Si le test communique réellement avec le réseau, cela signifie qu'il a une plus grande fidélité. En contrepartie, l'exécution du test peut prendre plus de temps, entraîner des erreurs si le réseau est en panne ou être coûteux.

Découvrez les éléments à tester pour découvrir comment définir votre stratégie de test.

Isolation et dépendances

Lorsque vous testez un élément ou un système d'éléments, vous le faites de manière isolée. Par exemple, pour tester un ViewModel, vous n'avez pas besoin de démarrer un émulateur et de lancer une UI, car il ne dépend pas (ou ne devrait pas) du framework Android.

Cependant, le sujet testé peut dépender d'autres sujets pour que le test fonctionne. Par exemple, un ViewModel peut dépendre d'un dépôt de données pour fonctionner.

Lorsque vous devez fournir une dépendance à un sujet soumis au test, une pratique courante consiste à créer un double de test (ou un objet de test). Les doubles de test sont des objets qui ressemblent à des composants de votre application et agissent en tant que composants dans votre application, mais qui sont créés dans votre test pour fournir un comportement ou des données spécifiques. Leur principal avantage est qu'ils simplifient et accélèrent les tests.

Types de doubles de test

Il existe différents types de doubles de test:

Falsifiée Double de test avec une implémentation "fonctionnelle" de la classe, mais implémentée de manière à être efficace pour les tests, mais inadaptée à la production.

Exemple: une base de données en mémoire.

Les faux ne nécessitent pas de framework de simulation et sont légers. Elles sont à privilégier.

Simulation Double de test qui se comporte de la manière dont vous le programmez et qui a des attentes concernant ses interactions. Les simulations échoueront aux tests si leurs interactions ne répondent pas aux exigences que vous définissez. Pour ce faire, les simulations sont généralement créées à l'aide d'un framework de simulation.

Exemple: Vérifier qu'une méthode dans une base de données a été appelée exactement une fois

Stub Double de test qui se comporte comme vous l'avez programmé, mais qui n'a pas d'attentes concernant ses interactions. Généralement créé avec un framework de simulation. Pour plus de simplicité, les faux sont préférables aux bouchons.
Factice Double de test transmis, mais non utilisé, par exemple si vous devez simplement le fournir en tant que paramètre.

Exemple: une fonction vide transmise en tant que rappel de clic.

Espion Wrapper sur un objet réel qui garde également une trace de certaines informations supplémentaires, comme pour les simulations. Elles sont généralement évités pour ajouter de la complexité. Les faux ou les sketches sont donc privilégiés par rapport aux espions.
Shadow Faux utilisé dans Robolectric.

Exemple utilisant un faux

Supposons que vous souhaitiez effectuer un test unitaire d'un ViewModel qui dépend d'une interface appelée UserRepository et qui expose le nom du premier utilisateur à une UI. Vous pouvez créer un double de test en implémentant l'interface et en renvoyant les données connues.

object FakeUserRepository : UserRepository {
    fun getUsers() = listOf(UserAlice, UserBob)
}

val const UserAlice = User("Alice")
val const UserBob = User("Bob")

Ce faux UserRepository n'a pas besoin de dépendre des sources de données locales et distantes que la version de production utiliserait. Le fichier se trouve dans l'ensemble de sources de test et ne sera pas envoyé avec l'application de production.

Une fausse dépendance peut renvoyer des données connues sans dépendre de sources de données distantes
Figure 1: fausse dépendance dans un test unitaire.

Le test suivant vérifie que le ViewModel expose correctement le premier nom d'utilisateur à la vue.

@Test
fun viewModelA_loadsUsers_showsFirstUser() {
    // Given a VM using fake data
    val viewModel = ViewModelA(FakeUserRepository) // Kicks off data load on init

    // Verify that the exposed data is correct
    assertEquals(viewModel.firstUserName, UserAlice.name)
}

Vous pouvez facilement remplacer UserRepository par un faux lors d'un test unitaire, car le ViewModel est créé par le testeur. Toutefois, il peut être difficile de remplacer des éléments arbitraires dans des tests plus importants.

Remplacer des composants et injection de dépendances

Lorsque les tests n'ont aucun contrôle sur la création des systèmes soumis aux tests, le remplacement des composants par les doubles de test devient plus complexe et nécessite que l'architecture de votre application suive une conception testable.

Même les tests de bout en bout de grande ampleur peuvent bénéficier de l'utilisation de doubles de test, tels qu'un test d'interface utilisateur instrumenté qui parcourt un flux utilisateur complet dans votre application. Dans ce cas, vous pouvez rendre votre test hermétique. Un test hermétique évite toutes les dépendances externes, telles que l'extraction de données sur Internet. Cela améliore la fiabilité et les performances.

Figure 2: Un test volumineux qui couvre la majeure partie de l'application et simule les données distantes.

Vous pouvez concevoir votre application manuellement pour obtenir cette flexibilité, mais nous vous recommandons d'utiliser un framework d'injection de dépendances tel que Hilt pour remplacer les composants de votre application au moment du test. Consultez le guide des tests Hilt.

Robolectric

Sous Android, vous pouvez utiliser le framework Robolectric, qui fournit un type spécial de double de test. Robolectric vous permet d'exécuter vos tests sur votre poste de travail ou dans votre environnement d'intégration continue. Il utilise une JVM standard, sans émulateur ni appareil. Il simule le gonflage des vues, du chargement des ressources et d'autres parties du framework Android avec des doubles de test appelés ombres.

Robolectric est un simulateur. Il ne doit donc pas remplacer les tests unitaires simples ni être utilisé pour effectuer des tests de compatibilité. Elle offre de la vitesse et réduit les coûts au détriment d'une fidélité inférieure dans certains cas. Une bonne approche pour les tests d'interface utilisateur consiste à les rendre compatibles à la fois avec les tests Robolectric et les tests d'instrumentation, et à décider quand les exécuter en fonction de la nécessité de tester la fonctionnalité ou la compatibilité. Les tests Espresso et Compose peuvent s'exécuter sur Robolectric.