Lors du développement d’applications, il est crucial de s’assurer que le code est de qualité et robuste grâce aux tests.
Les tests inutiles peuvent biaiser les métriques de couverture et ne pas garantir la fonctionnalité correcte de l’application.
Cet article explore comment écrire des tests pertinents dans une architecture à couches pour éviter ces pièges et assurer une couverture de tests efficace.
Développez des applications Android solides, fiables et efficaces avec une approche TDD
Lors du développement d’applications, la qualité et la robustesse du code reposent en grande partie sur les tests. Cependant, tous les tests ne se valent pas. Il est essentiel de comprendre quelle partie du code doit être testée et comment la tester correctement. Ce chapitre vous explique comment écrire des tests pertinents dans une architecture à couches, en prenant soin d’éviter les tests inutiles qui biaiseraient les métriques de couverture.
Tests Pertinents en Architecture à Couches
Le but des tests est de s’assurer que le code fonctionne comme prévu. En particulier, nous voulons vérifier que les scénarios d’utilisation de l’application (use cases) fonctionnent correctement, que les données sont bien manipulées et que l’interface réagit comme attendu.
Ce qu’il faut tester | Ce qu’il faut éviter |
---|---|
Scénarios d’utilisation (use cases) | Test des getters et setters (automatiques en Kotlin) |
Mappers (transformations entre couches) | Tests redondants déjà couverts par le langage |
View models (gestion des états et interactions UI) | Tester des détails non pertinents |
Optimiser la Couverture de Tests Unitaires
Définitions et Importance
La couverture des tests mesure dans quelle mesure votre code est testé. Cela inclut les classes, les fonctions et les branches conditionnelles. Cependant, une couverture de test élevée ne signifie pas forcément que votre code est bien testé. Il est possible d’avoir une couverture artificiellement élevée en testant des aspects sans importance réelle
Cette figure présente une hiérarchie des principes clés à prendre en compte lors de la mise en place de tests dans une application.
- En premier lieu, il est essentiel de s’assurer que les classes, fonctions, et branches pertinentes sont bien testées, car cela garantit une couverture fonctionnelle des principales logiques du code.
- Ensuite, l’ importance des tests significatifs , en rappelant que les tests doivent réellement valider des comportements critiques, plutôt que de simplement maximiser la couverture sans valeur ajoutée.
- Enfin, on compare le test réel (qui évalue de manière significative le comportement du code) à la couverture artificielle (qui pourrait être gonflée par des tests triviaux ou non pertinents), soulignant que la qualité des tests prime sur la simple quantité.
Exemple :
Prenons un modèle d’utilisateur simple. Tester la conservation des propriétés id et nom ne sert à rien si vous utilisez Kotlin, car ce comportement est garanti.
Ce code définit une data class en Kotlin appelée Utilisateur . Une data class est une classe spécifique en Kotlin qui est principalement utilisée pour stocker des données.
data class Utilisateur(val id : Int, val nom : String)
Il est inutile de tester ceci :
Ce code définit un test unitaire en Kotlin qui vérifie les propriétés d’un objet de la classe Utilisateur .
@Test fun testIdEtNomUtilisateur() {
val utilisateur = Utilisateur(1, "Jean") assertEquals(1, utilisateur.id)
assertEquals("Jean", utilisateur.nom)
}
Pourquoi ? En Kotlin, ces propriétés sont automatiquement conservées par le langage.
Use Cases en Architecture à Couches
Un use case (ou interactor ) est un concept clé dans une architecture orientée domaine, comme Clean Architecture ou MVVM (Model-View-ViewModel). Il représente une action métier spécifique ou un ensemble d’actions réalisées dans l’application. Les use cases encapsulent la logique métier de l’application, garantissant que chaque fonctionnalité est gérée de manière indépendante et réutilisable.
Cette figure illustre le concept d’un use case dans une application. Elle montre que le use case représente une ou plusieurs actions spécifiques, comme récupérer un utilisateur , réalisées dans l’application. Ces actions incluent souvent l’ interaction avec la base de données ou d’autres sources de données, mais encapsulent la logique métier de manière indépendante.
L’exemple typique présenté ici est celui de la récupération d’un utilisateur. Le use case orchestre la communication avec la base de données via un repository ou une autre couche d’accès aux données, tout en gardant la logique métier séparée des autres couches (comme l’interface utilisateur). Cela permet de structurer le code de manière modulaire et facilite la réutilisation et la testabilité.
Les use cases doivent être testés pour s’assurer qu’ils fonctionnent comme prévu. Un point important dans une architecture Android est la gestion asynchrone des exécutions. Vous devrez donc simuler les exécutions asynchrones pour tester efficacement.
Exemple : Test d’un use case de récupération d’utilisateur
Ce code en Kotlin définit un use case appelé GetUtilisateurUseCase qui récupère un utilisateur à partir d’un dépôt de données ( UtilisateurRepository ) en utilisant une méthode suspend pour des opérations asynchrones. Le test unitaire utilise Mockito pour simuler le comportement du repository . D’abord, un mock du repository est créé et un objet Utilisateur (avec un id de 1 et un nom « Jean ») est configuré pour être retourné lorsque la méthode getUtilisateur(1) est appelée sur ce repository simulé. Ensuite, la méthode execute du use case est appelée, et le résultat est comparé à l’utilisateur attendu via assertEquals . Ce test vérifie que le use case interagit correctement avec le repository et retourne l’utilisateur attendu.
class GetUtilisateurUseCase(private val repository : UtilisateurRepository) {
suspend fun execute(id : Int) : Utilisateur {
return repository.getUtilisateur(id)
}
} @Test fun testGetUtilisateurUseCase() = runBlocking {
val repository = mock(UtilisateurRepository::class.java) val useCase =
GetUtilisateurUseCase(repository) val utilisateur =
Utilisateur(1, "Jean")
`when`(repository.getUtilisateur(1))
.thenReturn(utilisateur)
val result =
useCase.execute(1) assertEquals(utilisateur, result)
}
Ici, nous avons testé que le use case renvoie l’utilisateur attendu, en simulant la base de données (repository).
La figure montre une erreur lors de l’exécution d’un test unitaire dans un environnement Android, visible dans la console de débogage de Android Studio . Le message « Test events were not received » indique que les événements de test n’ont pas été correctement capturés, ce qui signifie que le test n’a pas pu s’exécuter ou que ses résultats n’ont pas été reçus.
Tests de Mappers en Architecture à Couches
Un mapper est un composant qui convertit des objets d’une couche à une autre. Par exemple, il peut transformer un objet de la couche domaine en un objet de la couche présentation .
Cette figure illustre le rôle d’un mapper dans une architecture en couches, en particulier entre la couche domaine et la couche présentation .
Pour tester un mapper, il suffit de vérifier que la transformation des données entre les couches est correcte.
Exemple : Test d’un mapper de domaine à présentation
Ce code définit un test unitaire pour la classe UtilisateurMapper , qui convertit un objet de la couche domaine en un objet de la couche présentation (ViewModel). La data class UtilisateurDomaine représente un utilisateur dans la couche métier avec un id et un nom , tandis que UtilisateurViewModel est la version utilisée par l’interface utilisateur, où le nomComplet est utilisé. Le mapper UtilisateurMapper effectue la conversion entre ces deux objets via la méthode mapToViewModel . Le test crée un UtilisateurDomaine , utilise le mapper pour le convertir en UtilisateurViewModel , puis vérifie que les propriétés id et nomComplet sont correctement mappées à l’aide de assertEquals . Cela garantit que le mapper fonctionne correctement en convertissant les objets entre les couches.
data class UtilisateurDomaine(val id : Int, val nom : String) data
class UtilisateurViewModel(
val id : Int, val nomComplet : String) class UtilisateurMapper {
fun mapToViewModel(utilisateur : UtilisateurDomaine) : UtilisateurViewModel {
return UtilisateurViewModel(utilisateur.id, utilisateur.nom)
}
} @Test fun testUtilisateurMapper() {
val utilisateurDomaine = UtilisateurDomaine(1, "Jean") val mapper =
UtilisateurMapper() val utilisateurViewModel =
mapper.mapToViewModel(utilisateurDomaine)
assertEquals(utilisateurDomaine.id, utilisateurViewModel.id)
assertEquals(utilisateurDomaine.nom,
utilisateurViewModel.nomComplet)
}
Dans cet exemple, le mapper est testé pour s’assurer qu’il transforme correctement l’utilisateur du domaine en un objet pour la vue.
View Models : Tests Unitaires Efficaces
Un view model gère la logique de l’interface utilisateur (UI) en interagissant avec la couche de domaine. Il surveille l’état des données et met à jour l’UI en conséquence.
Cette figure illustre le flux de données et d’interactions entre les différentes couches d’une architecture d’application.
- Couche de Domaine :Elle représente la logique métier de l’application. C’est ici que les règles de gestion et les objets du domaine (comme les utilisateurs, les produits, etc.) sont définis. Cette couche est indépendante de l’interface utilisateur.
- ViewModel :LeViewModelsert de pont entre la couche de domaine et l’interface utilisateur. Il récupère les données de la couche de domaine (souvent via desUse Cases) et les prépare dans un format que l’UI peut consommer. Il est responsable de gérer l’état de l’interface et d’exposer les données prêtes à l’affichage.
- Interface Utilisateur (UI) :L’UI est responsable de la présentation des données et des interactions utilisateur. Elle observe les changements d’état dans leViewModelet affiche les données ou réagit aux actions de l’utilisateur en conséquence.
Pour tester un view model, il faut simuler les interactions asynchrones et vérifier que l’état du modèle est mis à jour correctement en réponse aux actions de l’utilisateur.
Exemple : Test d’un view model avec un use case simulé
Ce code définit une classe UtilisateurViewModel qui utilise un GetUtilisateurUseCase pour charger les informations d’un utilisateur et les exposer à l’interface utilisateur via un MutableLiveData appelé utilisateurState . La méthode chargerUtilisateur utilise le viewModelScope pour lancer une coroutine qui appelle le use case et met à jour l’état de l’utilisateur dans le LiveData . Le test unitaire, écrit avec runBlocking , simule ce comportement en utilisant un mock du GetUtilisateurUseCase . Le test configure le mock pour retourner un utilisateur spécifique lorsque le use case est exécuté avec l’ID 1. Après avoir appelé la méthode chargerUtilisateur(1) , le test vérifie que l’état de utilisateurState dans le ViewModel est correctement mis à jour avec l’utilisateur simulé. Ce test garantit que le ViewModel interagit correctement avec le use case et met à jour son état pour l’interface utilisateur.
class UtilisateurViewModel(
private val getUtilisateurUseCase : GetUtilisateurUseCase)
: ViewModel() {
val utilisateurState =
MutableLiveData() fun chargerUtilisateur(id : Int) {
viewModelScope.launch {
val utilisateur =
getUtilisateurUseCase.execute(id) utilisateurState.value = utilisateur
}
}
}
@Test fun testUtilisateurViewModel() = runBlocking {
val useCase = mock(GetUtilisateurUseCase::class.java) val viewModel =
UtilisateurViewModel(useCase) val utilisateur =
Utilisateur(1, "Alphorm user")
`when`(useCase.execute(1))
.thenReturn(utilisateur) viewModel.chargerUtilisateur(1)
assertEquals(utilisateur, viewModel.utilisateurState.value)
}
Ce test simule un cas d’utilisation pour charger un utilisateur et vérifie que l’état du view model est mis à jour correctement.
Cette image montre une interface utilisateur (UI) où les informations d’un utilisateur sont affichées, spécifiquement l’ID et le nom de l’utilisateur : User ID: 1, Name: Alphorm user . Cela suggère que les données proviennent d’une classe comme UtilisateurViewModel , qui a chargé un objet Utilisateur avec un ID de 1 et un nom « Alphorm user ».
Formez-vous gratuitement avec Alphorm !
Maîtrisez les compétences clés en IT grâce à nos formations gratuites et accélérez votre carrière dès aujourd'hui.
FAQ
Comment déterminer les parties du code à tester ?
Pourquoi la couverture de tests ne suffit-elle pas pour garantir la qualité du code ?
Comment les use cases améliorent-ils la modularité et la maintenabilité ?
Quel est le rôle des mappers dans une architecture en couches ?
Comment tester efficacement un view model ?
Conclusion
En conclusion, rédiger des tests unitaires pertinents est crucial pour garantir la qualité et la robustesse de votre code en architecture à couches. Quelle sera votre prochaine étape pour approfondir vos connaissances sur les tests unitaires et améliorer votre processus de développement?