Cet article est le dernier d'une série sur l'application du modÚle architectural MVI dans Kotlin Multiplatform. Dans les deux parties précédentes ( partie 1 et partie 2 ), nous nous sommes souvenus de ce qu'est MVI, avons créé un module générique Kittens pour charger des images de chat et l'avons intégré dans les applications iOS et Android.
Dans cette partie, nous aborderons le module Kittens avec des tests unitaires et d'intĂ©gration. Nous dĂ©couvrirons les limites actuelles des tests dans Kotlin Multiplatform, dĂ©couvrirons comment les surmonter et mĂȘme les faire fonctionner Ă notre avantage.
Un exemple de projet mis Ă jour est disponible sur notre GitHub .
Prologue
Il ne fait aucun doute que les tests sont une Ă©tape importante du dĂ©veloppement logiciel. Bien sĂ»r, cela ralentit le processus, mais en mĂȘme temps:
- vous permet de vérifier les cas de bord qui sont difficiles à attraper manuellement;
- Réduit les risques de régression lors de l'ajout de nouvelles fonctionnalités, de la correction de bugs et de la refactorisation;
- vous oblige à décomposer et structurer votre code.
à premiÚre vue, le dernier point peut sembler un inconvénient, car cela prend du temps. Cependant, cela rend le code plus lisible et plus avantageux à long terme.
«En effet, le rapport entre le temps passé à lire et à écrire est bien supérieur à 10 pour 1. Nous lisons constamment l'ancien code dans le cadre de l'effort d'écriture de nouveau code. ... [Par conséquent,] le rendre facile à lire le rend plus facile à écrire. " - Robert C. Martin, "Clean Code: A Handbook of Agile Software Craftsmanship"
Kotlin Multiplatform Ă©tend les capacitĂ©s de test. Cette technologie ajoute une caractĂ©ristique importante: chaque test est automatiquement effectuĂ© sur toutes les plates-formes prises en charge. Si, par exemple, seuls Android et iOS sont pris en charge, le nombre de tests peut ĂȘtre multipliĂ© par deux. Et si, Ă un moment donnĂ©, la prise en charge d'une autre plate-forme est ajoutĂ©e, elle est automatiquement couverte par les tests.
Les tests sur toutes les plates-formes prises en charge sont importants car il peut y avoir des différences dans le comportement du code. Par exemple, Kotlin / Native a un modÚle de mémoire spécial , Kotlin / JS donne aussi parfois des résultats inattendus.
Avant d'aller plus loin, il convient de mentionner certaines des limites des tests de Kotlin Multiplatform. Le plus important est l'absence de bibliothÚque moqueuse pour Kotlin / Native et Kotlin / JS. Cela peut sembler un gros inconvénient, mais je le considÚre personnellement comme un avantage. Tester dans Kotlin Multiplatform a été assez difficile pour moi: j'ai dû créer des interfaces pour chaque dépendance et écrire leurs implémentations de test (faux). Cela a pris du temps, mais à un moment donné, j'ai réalisé que passer du temps sur les abstractions est un investissement qui conduit à un code plus propre.
J'ai Ă©galement remarquĂ© que les modifications ultĂ©rieures de ce code prennent moins de temps. Pourquoi donc? Parce que l'interaction d'une classe avec ses dĂ©pendances n'est pas clouĂ©e (moquĂ©e). Dans la plupart des cas, il suffit de simplement mettre Ă jour leurs implĂ©mentations de test. Vous n'avez pas besoin d'approfondir chaque mĂ©thode de test pour mettre Ă jour vos simulations. En consĂ©quence, j'ai arrĂȘtĂ© d'utiliser des bibliothĂšques moqueuses mĂȘme dans le dĂ©veloppement Android standard. Je recommande de lire l'article suivant: " Se moquer n'est pas pratique - Utiliser des faux " de Pravin Sonawane .
Plan
Souvenons-nous de ce que nous avons dans le module Kittens et de ce que nous devons tester.
- KittenStore est le composant principal du module. Son implémentation KittenStoreImpl contient la plupart de la logique métier. C'est la premiÚre chose que nous allons tester.
- KittenComponent est la façade du module et le point d'intégration pour tous les composants internes. Nous couvrirons ce composant avec des tests d'intégration.
- KittenView est une interface publique qui représente la dépendance d'interface utilisateur de KittenComponent.
- KittenDataSource est une interface d'accÚs Web interne qui a des implémentations spécifiques à la plate-forme pour iOS et Android.
Pour une meilleure compréhension de la structure du module je vais donner son diagramme UML:
Le plan est le suivant:
- Tester KittenStore
- Création d'une implémentation de test de KittenStore.Parser
- Création d'une implémentation de test de KittenStore.Network
- RĂ©daction de tests unitaires pour KittenStoreImpl
- Création d'une implémentation de test de KittenStore.Parser
- Tester le composant Kitten
- Création d'une implémentation de test de KittenDataSource
- Créer une implémentation de test KittenView
- Rédaction de tests d'intégration pour KittenComponent
- Création d'une implémentation de test de KittenDataSource
- Exécution de tests
- conclusions
Tests unitaires KittenStore
L'interface KittenStore a sa propre classe d'implĂ©mentation - KittenStoreImpl. C'est ce que nous allons tester. Il a deux dĂ©pendances (interfaces internes), dĂ©finies directement dans la classe elle-mĂȘme. Commençons par Ă©crire des implĂ©mentations de test pour eux.
Tester l'implémentation de KittenStore.Parser
Ce composant est responsable des requĂȘtes rĂ©seau. Voici Ă quoi ressemble son interface:
Avant d'écrire une implémentation de test d'une interface réseau, nous devons répondre à une question importante: quelles données le serveur renvoie-t-il? La réponse est que le serveur renvoie un ensemble aléatoire de liens d'images, à chaque fois un ensemble différent. Dans la vraie vie, le format JSON est utilisé, mais comme nous avons une abstraction Parser, nous ne nous soucions pas du format dans les tests unitaires.
Une implĂ©mentation rĂ©elle peut changer de flux, de sorte que les abonnĂ©s peuvent ĂȘtre gelĂ©s dans Kotlin / Native. Ce serait formidable de modĂ©liser ce comportement pour vous assurer que le code gĂšre tout correctement.
Ainsi, notre implémentation de test de Network devrait avoir les caractéristiques suivantes:
- doit retourner un ensemble non vide de lignes diffĂ©rentes pour chaque requĂȘte;
- le format de rĂ©ponse doit ĂȘtre commun pour Network et Parser;
- devrait ĂȘtre en mesure de simuler des erreurs de rĂ©seau (peut-ĂȘtre devrait se terminer sans rĂ©ponse);
- il devrait ĂȘtre possible de simuler un format de rĂ©ponse invalide (pour vĂ©rifier les erreurs dans Parser);
- il devrait ĂȘtre possible de simuler les dĂ©lais de rĂ©ponse (pour vĂ©rifier la phase de dĂ©marrage);
- devrait ĂȘtre congelable dans Kotlin / Native (juste au cas oĂč).
L'implĂ©mentation du test elle-mĂȘme pourrait ressembler Ă ceci:
TestKittenStoreNetwork a un stockage de chaĂźnes (tout comme un vrai serveur) et peut gĂ©nĂ©rer des chaĂźnes. Pour chaque requĂȘte, la liste actuelle des lignes est codĂ©e en une seule ligne. Si la propriĂ©tĂ© "images" est nulle, alors Maybe se terminera simplement, ce qui devrait ĂȘtre considĂ©rĂ© comme une erreur.
Nous avons Ă©galement utilisĂ© TestScheduler . Ce planificateur a une fonction importante: il gĂšle toutes les tĂąches entrantes. Ainsi, l'opĂ©rateur observeOn, utilisĂ© en conjonction avec TestScheduler, gĂšlera le flux en aval, ainsi que toutes les donnĂ©es qui le traversent, comme dans la vraie vie. Mais en mĂȘme temps, le multithreading ne sera pas impliquĂ©, ce qui simplifie les tests et les rend plus fiables.
De plus, TestScheduler dispose d'un mode spécial "traitement manuel" qui nous permettra de simuler la latence du réseau.
Tester l'implémentation de KittenStore.Parser
Ce composant est responsable de l'analyse des réponses du serveur. Voici son interface:
Ainsi, tout ce qui est tĂ©lĂ©chargĂ© sur le Web doit ĂȘtre converti en une liste de liens. Notre rĂ©seau concatĂšne simplement les chaĂźnes en utilisant un sĂ©parateur point-virgule (;), utilisez donc le mĂȘme format ici.
Voici une implémentation de test:
Comme avec Network, TestScheduler est utilisé pour geler les abonnés et vérifier leur compatibilité avec le modÚle de mémoire Kotlin / Native. Les erreurs de traitement des réponses sont simulées si la chaßne d'entrée est vide.
Tests unitaires pour KittenStoreImpl
Nous avons maintenant des implĂ©mentations de test de toutes les dĂ©pendances. Il est temps pour les tests unitaires. Tous les tests unitaires peuvent ĂȘtre trouvĂ©s dans le rĂ©fĂ©rentiel , ici je ne donnerai que l'initialisation et quelques tests eux-mĂȘmes.
La premiÚre étape consiste à créer des instances de nos implémentations de test:
KittenStoreImpl utilise mainScheduler, donc l'Ă©tape suivante consiste Ă le remplacer:
Maintenant, certains tests peuvent ĂȘtre effectuĂ©s. KittenStoreImpl doit charger les images immĂ©diatement aprĂšs leur crĂ©ation. Cela signifie qu'une demande rĂ©seau doit ĂȘtre terminĂ©e, sa rĂ©ponse doit ĂȘtre traitĂ©e et l'Ă©tat doit ĂȘtre mis Ă jour avec le nouveau rĂ©sultat.
Ce que nous avons fait:
- images générées sur le réseau;
- créé une nouvelle instance de KittenStoreImpl;
- s'est assuré que l'état contient la liste correcte de chaßnes.
Un autre scĂ©nario que nous devons envisager est d'obtenir KittenStore.Intent.Reload. Dans ce cas, la liste doit ĂȘtre rechargĂ©e depuis le rĂ©seau.
Ătapes du test:
- générer des images sources;
- créer une instance de KittenStoreImpl;
- générer de nouvelles images;
- envoyer Intent.Reload;
- assurez-vous que la condition contient de nouvelles images.
Enfin, examinons le scénario suivant: lorsque l'indicateur isLoading est défini pendant le chargement des images.
Nous avons activé le traitement manuel pour TestScheduler - maintenant, les tùches ne seront pas traitées automatiquement. Cela nous permet de vérifier l'état en attendant une réponse.
Test d'intégration KittenComponent
Comme je l'ai mentionnĂ© ci-dessus, KittenComponent est le point d'intĂ©gration de l'ensemble du module. Nous pouvons le couvrir de tests d'intĂ©gration. Jetons un coup d'Ćil Ă son API:
Il existe deux dépendances, KittenDataSource et KittenView. Nous aurons besoin d'implémentations de test pour ces derniers avant de pouvoir commencer les tests.
Par souci d'exhaustivité, ce diagramme montre le flux de données à l'intérieur du module:
Tester l'implémentation de KittenDataSource
Ce composant est responsable des requĂȘtes rĂ©seau. Il a des implĂ©mentations distinctes pour chaque plateforme, et nous avons besoin d'une autre implĂ©mentation pour les tests. Voici Ă quoi ressemble l'interface KittenDataSource:
TheCatAPI prend en charge la pagination, j'ai donc ajoutĂ© les arguments appropriĂ©s tout de suite. Sinon, il est trĂšs similaire Ă KittenStore.Network, que nous avons implĂ©mentĂ© plus tĂŽt. La seule diffĂ©rence est que nous devons utiliser le format JSON car nous testons du vrai code en intĂ©gration. Nous empruntons donc simplement l'idĂ©e de mise en Ćuvre:
Comme prĂ©cĂ©demment, nous gĂ©nĂ©rons diffĂ©rentes listes de chaĂźnes qui sont encodĂ©es dans un tableau JSON Ă chaque requĂȘte. Si aucune image n'est gĂ©nĂ©rĂ©e, ou si les arguments de la requĂȘte sont erronĂ©s, Maybe se terminera simplement sans rĂ©ponse.
La bibliothÚque kotlinx.serialization est utilisée pour former un tableau JSON . à propos, le KittenStoreParser testé l' utilise pour le décodage.
Tester la mise en Ćuvre de KittenView
C'est le dernier composant pour lequel nous avons besoin d'une implémentation de test avant de pouvoir commencer les tests. Voici son interface:
C'est une vue qui prend juste des modÚles et déclenche des événements, donc son implémentation de test est trÚs simple:
Nous avons juste besoin de nous souvenir du dernier modÚle accepté - cela nous permettra de vérifier l'exactitude du modÚle affiché. Nous pouvons également distribuer des événements au nom de KittenView à l'aide de la méthode dispatch (Event), qui est déclarée dans la classe AbstractMviView héritée.
Tests d'intégration pour KittenComponent
L'ensemble complet des tests se trouve dans le référentiel , ici je ne donnerai que quelques-uns des plus intéressants.
Comme précédemment, commençons par instancier les dépendances et initialiser:
Actuellement, deux programmateurs sont utilisés pour le module: mainScheduler et computationScheduler. Nous devons les remplacer:
Nous pouvons maintenant écrire quelques tests. Vérifions d'abord le script principal pour nous assurer que les images sont chargées et affichées au démarrage:
Ce test est trÚs similaire à celui que nous avons écrit lorsque nous avons examiné les tests unitaires pour KittenStore. Ce n'est que maintenant que l'ensemble du module est impliqué.
Ătapes du test:
- générer des liens vers des images dans TestKittenDataSource;
- créer et exécuter KittenComponent;
- assurez-vous que les liens atteignent TestKittenView.
Autre scĂ©nario intĂ©ressant: les images doivent ĂȘtre rechargĂ©es lorsque KittenView dĂ©clenche l'Ă©vĂ©nement RefreshTriggered.
Ătapes:
- générer des liens sources vers des images;
- créer et exécuter KittenComponent;
- générer de nouveaux liens;
- envoyer Event.RefreshTriggered au nom de KittenView;
- assurez-vous que les nouveaux liens atteignent TestKittenView.
Exécution de tests
Pour exécuter tous les tests, nous devons effectuer la tùche Gradle suivante:
./gradlew :shared:kittens:build
Cela compilera le module et exécutera tous les tests sur toutes les plates-formes prises en charge: Android et iosx64.
Et voici le rapport de couverture JaCoCo:
Conclusion
Dans cet article, nous avons couvert le module Kittens avec des tests unitaires et d'intégration. La conception proposée du module nous a permis de couvrir les parties suivantes:
- KittenStoreImpl - contient la plupart de la logique métier;
- KittenStoreNetwork - responsable des requĂȘtes rĂ©seau de haut niveau;
- KittenStoreParser - responsable de l'analyse des réponses du réseau;
- toutes les transformations et connexions.
Le dernier point est trĂšs important. Il est possible de le couvrir grĂące Ă la fonctionnalitĂ© MVI. La seule responsabilitĂ© de la vue est d'afficher les donnĂ©es et d'envoyer des Ă©vĂ©nements. Tous les abonnements, conversions et liens se font Ă l'intĂ©rieur du module. Ainsi, nous pouvons tout couvrir avec des tests gĂ©nĂ©raux, Ă l'exception de l'affichage lui-mĂȘme.
Ces tests présentent les avantages suivants:
- n'utilisez pas d'API de plateforme;
- exécuté trÚs rapidement;
- fiable (ne clignote pas);
- s'exécuter sur toutes les plates-formes prises en charge.
Nous avons également pu tester la compatibilité du code avec le modÚle de mémoire complexe Kotlin / Native. Ceci est également trÚs important en raison du manque de sécurité au moment de la construction: le code se bloque juste à l'exécution avec des exceptions difficiles à déboguer.
J'espĂšre que cela vous aidera dans vos projets. Merci d'avoir lu mes articles! Et n'oubliez pas de me suivre sur Twitter .
...
Exercice bonus
Si vous souhaitez travailler avec des implémentations de test ou jouer avec MVI, voici quelques exercices pratiques.
Refactoriser le KittenDataSource
Il existe deux implĂ©mentations de l'interface KittenDataSource dans le module: une pour Android et une pour iOS. J'ai dĂ©jĂ mentionnĂ© qu'ils sont responsables de l'accĂšs au rĂ©seau. Mais ils ont en fait une autre fonction: ils gĂ©nĂšrent l'URL de la requĂȘte en fonction des arguments d'entrĂ©e "limite" et "page". En mĂȘme temps, nous avons une classe KittenStoreNetwork qui ne fait rien d'autre que de dĂ©lĂ©guer l'appel Ă KittenDataSource.
Affectation: déplacez la logique de génération de demande d'URL de KittenDataSourceImpl (sur Android et iOS) vers KittenStoreNetwork. Vous devez modifier l'interface KittenDataSource comme suit:
Une fois que vous avez fait cela, vous devrez mettre Ă jour vos tests. La seule classe que vous devez toucher est TestKittenDataSource.
Ajout du chargement de la page
TheCatAPI prend en charge la pagination, nous pouvons donc ajouter cette fonctionnalitĂ© pour une meilleure expĂ©rience utilisateur. Vous pouvez commencer par ajouter un nouvel Ă©vĂ©nement Event.EndReached pour KittenView, aprĂšs quoi le code arrĂȘtera la compilation. Ensuite, vous devrez ajouter le Intent.LoadMore appropriĂ©, convertir le nouvel Ă©vĂ©nement en Intent et traiter ce dernier dans KittenStoreImpl. Vous devrez Ă©galement modifier l'interface KittenStoreImpl.Network comme suit:
Enfin, vous devrez mettre à jour certaines implémentations de test, corriger un ou deux tests existants, puis en écrire de nouveaux pour couvrir la pagination.