Publié par
Il y a 3 années · 12 minutes · Android, Mobile

Plus d’excuses, les tests en Android c’est possible

Dans la communauté Android, l’accès à des exemples, des tutoriels, des articles sur le dernier widget ou la dernière librairie est monnaie courante. Cependant, il est très difficile de se procurer de bons articles ou de la documentation sur la mise en place d’une architecture de tests pour une application Android.

On entend souvent “Les tests en Android, c’est un vrai casse tête !” voire « C’est impossible !”. Le but de cet article est de vous montrer qu’en 2015, il est possible de tester facilement et efficacement une application Android. Vous n’aurez plus aucune excuse !

Historique et idées reçues

Quand nous regardons la documentation du SDK Android pour réaliser des tests, on trouve la classe ActivityInstrumentationTestCase2. Elle permet de mettre en place des tests s’exécutant sur le device.

“Avec le nom de cette classe, difficile d’être rassuré. Tester unitairement une Activity (UI) ? Sur un device ? Instrumentation ? 2 ?”

Cette situation a donné lieu à de nombreuses idées reçues sur l’écriture des tests en Android :

Il faut forcément un device connecté pour faire des tests : Et bien non ! Un device n’est pas toujours nécessaire mais il reste intéressant d’exécuter certains tests directement sur celui-ci afin de garantir des comportements notamment graphiques. Le principal problème avec l’exécution sur un device est qu’il ne s’agit plus réellement de tests unitaires. Son exécution va être aussi terriblement lente.

Ces problèmes sont à l’origine de la création de Robolectric. Cette librairie a pour but de mocker une partie du framework Android afin de permettre l’exécution des tests directement sur une JVM en chargeant dans le classloader les différentes classes Android nécessaires. Deuxième idée reçue : “Robolectric c’est compliqué, difficile à mettre en place et il y a des shadows dans tous les sens”. Il est vrai que Robolectric a mis du temps à se stabiliser au niveau de l’API mais depuis la sortie de la version 2 (et même 3.0 dernièrement), la clarté des API et la simplicité de mise en place se sont considérablement améliorées.

On ne peut pas exécuter les tests dans l’IDE, ce qui implique des problèmes d’efficacité dans l’écriture des tests et leur débuggage. Avec l’arrivée d’AndroidStudio et du plugin Gradle 1.1.0, les tests unitaires sont maintenant supportés directement dans l’IDE. Compilation, exécution partielle, debuggage, tout y est.

Tests unitaires vs tests fonctionnels

Afin de comprendre la suite de cet article, il est important de bien différencier les tests unitaires des tests fonctionnels ou d’intégration. Les tests unitaires sont un moyen de tester une toute petite partie de code contenant la logique technique, indépendamment des autres. Au contraire, les tests fonctionnels ou d’intégration vont permettre de tester des parcours utilisateurs.

La question à se poser est donc : quand faire des tests unitaires et quand faire des tests fonctionnels ?

Dans notre conception des TU, il est important que ceux-ci ne soient pas dépendants des cycles de vie de l’application. Idéalement, ils doivent être le plus détachés possible du framework. Ils doivent être brefs, rapides et amener une vraie plus- value. En effet, attention à ne pas tester Android, cela n’a aucun intérêt. Robolectric est donc à utiliser avec parcimonie. Tester par exemple, les services Android (TelephonyManager, ConnectivityManager, etc.) n’apporte rien, mais tester leur utilisation en les mockant a beaucoup plus de sens.

Toujours dans cette conception, les tests fonctionnels vont permettre de s’intégrer avec le framework Android et ainsi tester des parcours utilisateurs complets, vérifier que les Activity s’enchaînent correctement et affichent les bonnes informations à l’utilisateur. Il me paraît alors important de définir avec une équipe métier l’ensemble des chemins critiques qui font la vraie valeur de l’application.

Adopter une architecture adaptée aux tests

Afin de rendre une application testable, il nous faut adopter une architecture modulaire et bien découpée pour faciliter les tests de chacune des parties. La suite de cet article va s’appuyer sur une architecture MVP (Model-View-Presenter) que je vais présenter brièvement :

  • Model : Il représente la donnée qui va être affichée. Il peut également souvent représenter le fournisseur ou la source de données.
  • View : Elle représente l’interface graphique qui va afficher la donnée. Elle va également permettre de réaliser les bonnes actions liées aux interactions de l’utilisateur, un clic par exemple.
  • Presenter : Il s’agit de l’homme du milieu. C’est l’organisateur. En fonction des interactions reçues de la View, il va récupérer et afficher les données du Model.

On peut se demander pourquoi l’architecture MVP est un réel atout dans l’écriture de tests en Android. Elle permet en fait de bien gérer les responsabilités de chaque partie par la définition des trois rôles, ainsi l’architecture de notre application est compatible avec la mise en place de tests puisque modulaire et découpée.

Afin de mieux illustrer une architecture, rien de mieux qu’un cas concret : Installed Apps. Le projet est disponible sur Github. Le but de cette application Android est très simple. Elle affiche une liste des applications installées sur votre device Android. Elle contient également un champ permettant de les filtrer par nom.

 

 

Le projet ne contient que 4 classes :

  • App : Cette classe appartient au Model. Elle représente une application installée. Elle va être facilement testable unitairement.
  • AppsProvider : Cette classe fournit la donnée, c’est-à-dire la liste des applications installées. Elle est difficilement testable car complètement liée au système Android via le PackageManager. La tester reviendrait à tester Android. Son comportement sera donc vérifié par les tests fonctionnels.
  • AppsActivity : C’est notre Activity, elle représente notre View. Son but est d’afficher les données fournies. Elle est évidemment fortement couplée au cycle de vie et elle aussi vérifiée par les tests fonctionnels.
  • AppsPresenter : C’est notre Presenter. Son but est de répondre aux interactions de l’utilisateur (filtre) transmises par la AppsActivity et de réagir en conséquence. La tester est primordial puisque l’intelligence de l’application y réside. Grâce à l’architecture MVP, elle est facilement testable unitairement car elle ne contient aucun lien direct au framework Android.

Bon et maintenant concrètement, comment on les écrit ces tests ?

Passons maintenant à l’écriture de nos tests. La première étape est de mettre en place notre build.gradle. Vous trouverez sous ce lien un bootstrap contenant l’ensemble des dépendances et configurations nécessaires.

Tests unitaires

Commençons par l’écriture de nos tests unitaires. Premièrement, il vous faut ajouter à votre build.gradle :

 

// UNIT TESTING
testCompile(
 'junit:junit:4.11',
    'com.android.support:support-annotations:22.0.0',
    'com.squareup.assertj:assertj-android:1.0.0',
    'org.mockito:mockito-core:1.9.5',
 'org.assertj:assertj-core:1.7.0'
)
testCompile('org.robolectric:robolectric:2.4') {
 exclude group: 'commons-logging'
 exclude group: 'org.apache.httpcomponents'
}

 

On y trouve des dépendances vers des librairies classiques servant aux tests : JUnit, Mockito, AssertJ et également vers Robolectric.

Nous avions vu que deux classes semblaient testables unitairement : App et AppsPresenter. On les retrouve donc dans le projet de tests unitaires (src/test) : AppTest et AppsPresenterTest. On notera que AppTest utilise un runner Robolectric via l’annotation @RunWith(RobolectricTestRunner.class). En effet, dans ce cas, Robolectric est nécessaire puisque nous souhaitons vérifier notre implémentation de Parcelable, en plus des autres méthodes. Ce test est un très bon exemple de test unitaire où Robolectric apporte une vraie plus-value. En effet, nous ne testons pas l’implémentation Android, mais bel et bien notre implémentation de Parcelable, à savoir l’ajout et la récupération des données dans le Parcel.

@Test
public void should_restore_from_parcelable() {
 // Given
 App app = new App("MyTitle");

 // When
 Parcel parcel = Parcel.obtain();
 app.writeToParcel(parcel, 0);
 parcel.setDataPosition(0);

 // Then
 App fromParcel = App.CREATOR.createFromParcel(parcel);
 assertThat(fromParcel.title).isEqualTo("MyTitle");
}

La classe AppsPresenterTest est également très importante. En effet, c’est elle qui finalement contient toute l’intelligence de notre application. Cette classe est totalement testable. Elle utilise l’annotation @RunWith(MockitoJUnitRunner.class) afin de pouvoir utiliser Mockito. Nous mockons notre AppsActivity ainsi que notre AppsProvider. Mockito nous permet de valider le comportement de l’ensemble de nos méthodes notamment en vérifiant que certains appels sur AppsActivity sont bien réalisés. Il nous permet également de maîtriser les données traitées en mockant les appels au AppsProvider.

Comme on peut le voir, ces tests sont très simples et leur but n’est que d’illustrer cet article. On s’aperçoit toutefois que l’ensemble de notre logique est bien testée unitairement, permettant ainsi de limiter le nombre de bugs sur les parties techniques grâce aux TU et métiers grâce aux TI.

Tests fonctionnels

Encore une fois, commençons par le build.gradle. Il vous faut ajouter les dépendances et configurations suivantes:

// INTEGRATION TESTING
configurations {
 androidTestCompile.exclude module: 'support-v4'
 androidTestCompile.exclude module: 'support-v7'
 androidTestCompile.exclude module: 'android'
}
androidTestCompile('com.android.support.test.espresso:espresso-core:2.0',
 'com.android.support.test.espresso:espresso-idling-resource:2.0',
 'com.android.support.test:testing-support-lib:0.1',
 'com.squareup.spoon:spoon-client:1.1.8')
androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0') {
 exclude group: 'com.android.support', module: 'support-annotations'
}

On peut voir que nous utilisons Espresso en version 2. Espresso nous permet de jouer des tests directement sur le device. En effet, il permet de lancer notre application puis de générer des actions utilisateurs maîtrisées. Il permet également de valider certaines parties de l’affichage de nos vues. Il est donc le compagnon idéal de nos tests fonctionnels. Afin de terminer sa configuration, il nous faut rajouter dans notre build.gradle, un changement de runner afin de forcer Gradle à utiliser celui d’Espresso:

defaultConfig {
 applicationId "fr.xebia.jmartinez.installed"
 minSdkVersion 15
 targetSdkVersion 22
 versionCode 1
 versionName "1.0"
 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

Nous utilisons également Spoon, outil développé par Square, qui permet de prendre des screenshots au cours des tests et de générer un rapport visuel de l’ensemble des parcours testés par notre application.

Il est à noter que nous aurions tout à fait pu utiliser Robotium. Nous avons choisi d’utiliser Espresso car elle est la solution poussée par Google (intégrée dans les librairies de supports) et que son API nous semble plus facilement utilisable et extensible.

Passons maintenant à l’écriture de ces tests. Notre application étant simple, un seul test suffit pour valider l’ensemble des comportements. Ce test est contenu dans la classe AppsActivityTest (contenu dans le projet de tests fonctionnels src/androidTest) :

@Test
public void testFilterInstalledApp() {
 onView(withId(R.id.filter)).check(matches(isDisplayed()));
 takeScreenshot("Display_All_Installed_Apps");
 onView(withId(R.id.filter)).perform(typeText("Installed"));
 onView(withText(R.string.app_name)).check(matches(isDisplayed()));
 takeScreenshot("Display_Filtered_Installed_Apps");
}

Comme on peut le voir, son écriture est simple et très parlante grâce à l’API de Espresso. Dans un premier temps, nous vérifions que le champ de filtre est bien affiché. Nous sommes donc sûrs que notre AppsActivity s’est bien lancée et nous prenons une screenshot grâce à Spoon. Nous tapons ensuite les caractères “Installed” dans le champ de texte et nous vérifions que le nom de notre application est bien présent dans la liste. Nous prenons ensuite un nouveau screenshot. Notre parcours critique a bel et bien été testé ainsi que l’ensemble des classes : AppsProvider et AppsActivity.

Enfin, vous trouverez également dans un package internal, trois classes très utiles:

  • JUnitTestCase : Il s’agit de la classe mère de notre classe de tests. Elle fournit plusieurs méthodes utilitaires pour traiter les Activity et leur cycle de vie dans Espresso. Elle contient également l’utilisation des deux Rules JUnit 4.
  • ActivityRule : Il s’agit d’une Rule JUnit 4 et a été développée par Jake Wharton. Elle permet tout simplement de lancer une activity à chaque fois que nos tests démarrent et ce de façon plus simple et plus propre.
  • SpoonRule : Il s’agit également d’une Rule JUnit 4. Son but est de permettre à Spoon de prendre des screenshots dans nos tests.

Et dans l’IDE ça fonctionne aussi ?

Et bien oui ! Enfin ! Avec l’arrivée du plugin Gradle et d’Android Studio 1.1.0, les tests sont maintenant supportés directement dans l’IDE. Même si cette fonctionnalité est encore au stade “expérimental”, elle est totalement utilisable et fonctionne très bien. Les screenshots ci-dessous en attestent :

Tests unitaires

Tests fonctionnels

Comme on peut le voir, il suffit de sélectionner le “Test Artifact” adéquat : Unit Tests ou Android Instrumentation Tests. Ce choix permet de rendre compilable et exécutable le type de test sélectionné. A noter donc que les deux ne sont pas possibles en même temps pour l’instant.

Pour aller plus loin…

Il existe de nombreux outils vous permettant d’améliorer vos tests en Android. En effet, afin de pouvoir monitorer et avoir des métriques sur votre couverture de tests, il est intéressant de mettre en place un outil comme Jacoco par exemple. Si vous souhaitez que vos parcours utilisateurs soient écrits directement par vos QA ou des personnes non-techniques, un outil comme Cucumber (utilisable via Calabash par exemple) devient intéressant à mettre en place. Enfin, une librairie comme Gwen, implémenté par Shazam, vous permettra de donner une cohérence et une vraie lisibilité dans l’écriture de vos tests et ainsi augmenter leur maintenabilité et leur compréhension.

Ce ne sont que quelques exemples de librairies disponibles et mises à disposition de la communauté dans le but de toujours améliorer vos tests. Alors pourquoi ne pas y réfléchir, les évaluer et les mettre en place si cela vous paraît pertinent ?

Conclusion

Nous avons donc pu voir qu’il est désormais possible de mettre en place facilement, en adoptant une architecture modulaire et bien découpée, un ensemble de tests. Ces tests permettent d’atteindre des couvertures tout à fait honorables en Android et cela avec tout le confort d’un IDE. Il n’existe donc plus de raisons ou d’excuses pour ne pas le mettre en place dans vos applications.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *