Tests unitaires instrumentés de la vue dans une architecture MVP

Dans les deux précédents articles, nous avons vu comment les tests unitaires Java nous permettaient d’effectuer des tests unitaires sur le modèle et le présentateur. Aujourd’hui, nous allons voir comment effectuer des tests unitaires instrumentés afin de tester la vue.

Articles précédents :

  1. Tests unitaires Java simples sous Android
  2. Tests unitaires Java du présentateur dans une architecture MVP

Projet d’exemple

Le projet dont nous nous servirons pour cet article sera le même que celui dont nous nous sommes servi pour l’article précédent.

Il s’agit d’une application affichant une question choisie au hasard parmi deux avec quatre possibilités de réponse affichées dans un ordre aléatoire. Lorsqu’une réponse est sélectionnée, l’application affiche un message en fonction de l’exactitude de la réponse.

Le code source de l’application est disponible sur GitHub.

Qu’est ce que les tests instrumentés

Sous Android, les tests instrumentés sont les tests qui ont besoin d’être effectués sur un équipement Android et ne peuvent être effectués sur la JVM de votre machine.

Cette obligation, dans le cadre des tests unitaires, est rencontrée dans la majorité des cas dès lors que l’on veut une instance de l’objet Context associé à notre application pour effectuer nos tests.

Ainsi, lorsque vous exécuterez des tests instrumentés, Android Studio vous demandera sur quel équipement Android (virtuel ou réel) vous souhaitez effectuer ces tests, à travers la même boîte de dialogue que lorsque vous exécutez une application normalement.

Pour les exécuter, il faut faire un clic droit sur le paquet suffixé par (androidTest) et choisir “Run ‘tests in ‘packageName’’”. En raison de dépendances manquantes, le test ne pourra être exécuté en l’état actuel.

Mise en place des tests unitaires instrumentés

Notre but est de tester les cas suivant :

Lorsque la méthode showQuestion() est appelée, alors les vues Android concernées ont bien les bons textes et les bons tags.
Lorsque la méthode showMessage() est appelée, alors la vue de résultat est visible et a bien le bon texte dans la bonne couleur.

Ajout des dépendances

Pour pouvoir effectuer les tests instrumentés proposés dans cet article, vous aurez besoin d’ajouter les dépendances à JUnit et Espresso dans le fichier build.gradle du module :

De même que nous avons vu que la mention testCompile permet d’inclure des dépendances à des bibliothèques qui ne seront ajoutés que pour les tests unitaires Java, la mention androidTestCompile permet d’ajouter des dépendances qui ne seront ajoutées que pour les tests instrumentés.

Écriture du test

Nous allons commencer par mettre en place un test unitaire instrumentés sous Android Studio. Pour ce faire, renommez la classe ExampleInstrumentedTest existante en QuizzFragmentInstrumentedUnitTest. Une fois que c’est fait, renommez la méthode useAppContext() en fragment_showQuestion().

Une fois ceci fait, nous allons devoir ajouter une propriété privée mQuizzFragment, ainsi qu’une méthode setup() avec l’annotation @Before pour simuler l’appel à onCreateView().

Les méthodes de cycle de vie des Fragment et des Activity ne peuvent être testés avec des tests unitaires sous Android. La seule manière de les tester est via les tests d’interface automatiques. Il convient donc de mettre le minimum de code indispensable dans ces méthodes.

La méthode setup() se contentera donc d’initialiser les propriétés de l’objet QuizzFragment utilisé comme le fait la méthode onCreateView() :

Ici, nous nous sommes contenter de créer des instances des objets assignés par la méthode onCreateView(). Ainsi, nous pouvons effectuer des tests unitaires sur le fragment “comme si la méthode onCreate()” avait été appelée.

Nous pouvons maintenant remplir la méthode fragment_showQuestion() avec les tests à effectuer, à savoir la vérification du contenu des vues ainsi que des tags des boutons radio.

Vous pouvez exécuter les tests unitaires instrumentés, qui doivent normalement passer.

Nous allons maintenant effectuer un test pour vérifier que la méthode QuizzFragment.showMessage() agisse bien comme prévu. Mais en regardant le code de cette méthode, on voit qu’elle effectue un appel à getContext(). Hors, cette méthode renverra null tant que le fragment n’est pas rattaché à une Activity, dans le cycle de vie d’un Fragment, que nous avons décidé d’ignorer pour les tests unitaires. Du coup, nous allons devoir contourner ce problème pour pouvoir tester cette méthode.

Utilisation de Mockito

Nous ne détaillerons pas ici ce qu’est Mockito ni son intérêt. Vous pouvez vous lire l’article précédent pour une introduction détaillée à Mockito.

Nous allons commencer par ajouter la dépendance à Mockito dans Gradle. Attention : il s’agit d’une bibliothèque différente de celle utilisée pour les tests unitaires Java.

Une fois ceci fait, nous allons effectuer un mock de QuizzFragment dans la méthode setup() de QuizzFragmentInstrumentedUnitTest.

Attention : J’insiste encore une fois. Si vous décidez de créer un mock d’une classe que vous souhaitez tester, il est indispensable que les méthodes que vous testez garde leur comportement original. Sans quoi, le test n’a plus aucun intérêt.

Exercice

Vous devriez avoir normalement toutes les clés en main pour écrire une méthode de test fragment_showMessage() qui testera la méthode FragmentQuizz.showMessage().

Petite précision : Le but d’un test unitaire est de tester un morceau de code bien spécifique (ici la méthode showMessage()). Ainsi, seul un changement de code dans la méthode showMessage() doit pouvoir faire échouer le test. Pour être plus précis, un changement dans les ressources par exemple, ne doit pas faire échouer ce test.

Solution : La solution a cet exercice est disponible dans ce commit sur GitHub.

Limitations

Les tests unitaires ne peuvent pas tout tester sous Android. Par exemple, le cas suivant, bien qu’il serait pratique à tester, ne peut pas l’être :

Lorsque le bouton radio devient checked, alors la méthode QuizzPresenter.onAnswerSelected() est bien appelée avec le bon argument.

Le but de ce cas serait de tester que la méthode init() effectue bien le setOnCheckedChangeListener() sur chacun des boutons radio, avec le bon appel. Mais deux limites sur Android nous empêchent de tester ce cas :

  • L’objet RadioButton ne dispose d’aucune méthode de type getOnCheckedChangeListener() nous permettant d’interagir avec ce dernier qui est private.
  • La méthode setChecked() effectue des opérations UI (comme par exemple le fait d’afficher un point à l’intérieur du bouton lorsqu’il est sélectionné, et de le faire disparaître lorsqu’il est déselectionné), opérations qui nécessitent d’être dans un cycle de vie de Fragment que nous avons décidé d’ignorer.

Quand je dis “ne peuvent pas”, j’entends “ne peuvent raisonnablement pas”, bien entendu que cela est possible en créant une classe qui hérite de RadioButton et gère elle même le OnCheckedChangedListener, éventuellement couplé à une méthode setChecked(boolean callParent) qui permettrait d’effectuer ce test. Mais cela ajouterait une complexité au code uniquement pour ajouter des tests unitaires, comme une volonté déraisonnable d’atteindre les 100% de couverture de code.

J’entends souvent dire : “il ne faut jamais modifier le code uniquement dans l‘unique but d’effectuer des tests unitaires”. J’ai personnellement du mal avec cette affirmation, que je nuancerai plutôt par : “il ne faut jamais complexifier votre code pour effectuer des tests unitaires”. Maintenant, si vous avez la nécessité d’ajouter un getter (par exemple pour Mockito) ou de diviser des méthodes pour rendre possibles des tests unitaires, je suis plutôt en accord.

Et la couverture de code ?

Une petite chose qui peut-être ne vous aura pas échapper, c’est que l’option “Run ‘Tests in ‘samplequizz’’ with Coverage” n’est pas disponible pour les tests instrumentés. Dès lors, si vous souhaitez vérifier la couverture de code de vos tests instrumentés, nous allons devoir utiliser la librairie Jacoco (pour Java Code Coverage).

Pour ce faire, ajoutez la ligne suivante juste après la série de lignes commençant par ‘apply plugin:’ du fichier build.gradle de votre module.

Maintenant, ajoutez les lignes suivantes à la fin de ce même fichier build.gradle :

Voilà, Jacoco est en place. Maintenant, nous allons indiquer que les tests avec Couverture de code sont actifs pour le build type Debug.

Voilà, tout est en place. Dans la partie Gradle d’Android Studio, sous l’onglet verification des Tasks, vous devriez voir une tâche nommée createDebugCoverageReport.

Double-cliquez dessus pour l’exécuter.

Une fois que la tâche est terminée, le rapport de test se trouve dans le répertoire /build/reports/coverage/debug depuis la racine de votre module. En ouvrant le fichier index.html, vous aurez le détail de votre couverture de code.

Si vous cliquez sur le package net.gahfy.samplequizz de ce rapport, nous pouvons avoir une autre indication :

Ici, nous voyons que seul le code de QuizzFragment est couvert (en partie). Ce qui nous permet d’avoir déjà une première indication sur la qualité de nos tests, puisqu’ils sont bien “unitaires”. Pour s’en assurer, vous pouvez rentrer dans QuizzFragment, et observer que seul 100% du code des méthodes que l’on a testées sont couvertes, il n’y a aucune couverture de code sur les autres méthodes. Nous avons terminé notre tour d’horizon des tests unitaires.

Le code final de cet article est disponible dans la branche viewUnitTests du projet.

Conclusion sur les tests unitaires

“test unitaire” est une expression qui comporte deux mots, car elle remplit deux objectifs :

  • Le mot “test” de l’expression sert à valider. C’est la partie de l’expression qui ne sert qu’en cas de succès. En cas de succès, je suis certain que le code couvert produit les résultats attendus.
  • Le mot “unitaire” de l’expression sert à identifier. C’est la partie de l’expression qui ne sert qu’en cas d’échec. Si tel test est en échec, alors je suis certain que l’erreur se trouve dans telle partie très précisément de mon code. Pas besoin de chercher ailleurs, je gagne donc un temps énorme lors de la recherche du problème.

La partie “unitaire” des tests est malheureusement ignorée dans 90% (sinon 99%) des projets Android. Hors, c’est une négligence qui a de très grave répercussions lorsque l’on sait que la maintenance représente la majorité du temps consacrée à un projet. Savoir identifier très précisément d’où vient une erreur est crucial afin d’économiser ce temps.

Comme nous l’avons vu, les tests unitaires, bien qu’indispensables à la maintenance d’un projet, montrent rapidement leurs limites :

  • Comme nous l’avons vu dans cet article et le précédent, il est parfois “non souhaitable” d’effectuer des tests unitaires sur certaines portions de code, car ces derniers entraîneraient une complexification inutile du code pour pouvoir être effectués.
  • Point le plus important : dans une application, le code ne fait pas tout. Il convient de s’assurer que l’application fonctionne comme espéré, indépendamment des résultats produits par tel ou tel morceau de code.

C’est pourquoi nous verrons comment effectuer d’autres types de tests dans un prochain article.