Tutoriel de test Android : test unitaire comme un véritable droïde vert

Publié: 2022-03-11

En tant que développeurs d'applications expérimentés, à mesure que les applications que nous développons mûrissent, nous avons le sentiment qu'il est temps de commencer les tests. Les règles métier impliquent souvent que le système doit assurer la stabilité entre les différentes versions. Nous souhaitons également idéalement automatiser le processus de construction et publier l'application automatiquement. Pour cela, nous avons besoin d'outils de test Adnroid en place pour garantir que la construction fonctionne comme prévu.

Les tests peuvent fournir un niveau de confiance supplémentaire sur les choses que nous construisons. Il est difficile (voire impossible) de créer un produit parfait et sans bug. Par conséquent, notre objectif sera d'améliorer nos chances de succès sur le marché en mettant en place une suite de tests qui détectera rapidement les bogues nouvellement introduits dans notre application.

Tutoriel de test Android

En ce qui concerne Android et les différentes plates-formes mobiles en général, les tests d'applications peuvent être un défi. La mise en œuvre de tests unitaires et le respect des principes de développement piloté par les tests ou similaires peuvent souvent sembler peu intuitifs, pour le moins. Néanmoins, les tests sont importants et ne doivent pas être pris pour acquis ou ignorés. David, Kent et Martin ont discuté des avantages et des inconvénients des tests dans une série de conversations compilées dans un article intitulé "Le TDD est-il mort ?". Vous pouvez également y trouver les conversations vidéo réelles et obtenir plus d'informations si les tests correspondent à votre processus de développement et dans quelle mesure pouvez-vous les intégrer, dès maintenant.

Dans ce tutoriel de test Android, je vais vous guider à travers l'unité et l'acceptation, les tests de régression sur Android. Nous nous concentrerons sur l'abstraction de l'unité de tests sur Android, suivie d'exemples de tests d'acceptation, en mettant l'accent sur le fait de rendre le processus aussi rapide et simple que possible pour raccourcir les cycles de retour développeur-QA.

Dois-je le lire?

Ce tutoriel va explorer les différentes possibilités lorsqu'il s'agit de tester des applications Android. Les développeurs ou les chefs de projet qui souhaitent mieux comprendre les possibilités de test actuelles de la plate-forme Android peuvent décider d'utiliser ce didacticiel s'ils souhaitent adopter l'une des approches mentionnées dans cet article. Cependant, ce n'est pas une solution miracle, car la discussion impliquée dans un tel sujet varie intrinsèquement d'un produit à l'autre avec les délais, la qualité de la base de code du code, le niveau de couplage du système, la préférence du développeur en matière de conception d'architecture, la durée de vie projetée de la fonctionnalité à essai, etc...

Penser en unités : tests Android

Idéalement, nous voulons tester une unité/composant logique d'une architecture indépendamment. De cette façon, nous pouvons garantir que notre composant fonctionne correctement pour l'ensemble d'entrées que nous attendons. Les dépendances peuvent être simulées, ce qui nous permettra d'écrire des tests qui s'exécutent rapidement. De plus, nous pourrons simuler différents états du système en fonction de l'entrée fournie au test, couvrant des cas exotiques dans le processus.

L'objectif des tests unitaires Android est d'isoler chaque partie du programme et de montrer que les parties individuelles sont correctes. Un test unitaire fournit un contrat écrit strict auquel le morceau de code doit satisfaire. En conséquence, il offre plusieurs avantages. -Wikipédia

Roboélectrique

Robolectric est un framework de tests unitaires Android qui vous permet d'exécuter des tests à l'intérieur de la JVM sur votre poste de travail de développement. Robolectric réécrit les classes du SDK Android au fur et à mesure de leur chargement et leur permet de s'exécuter sur une JVM normale, ce qui accélère les temps de test. En outre, il gère l'inflation des vues, le chargement des ressources et d'autres éléments implémentés dans le code C natif sur les appareils Android, ce qui rend obsolète le besoin d'émulateurs et d'appareils physiques pour exécuter des tests automatisés.

Mockito

Mockito est un framework moqueur qui nous permet d'écrire des tests propres en Java. Il simplifie le processus de création de doublons de test (simulacres), qui sont utilisés pour remplacer les dépendances d'origine d'un composant/module utilisé en production. Une réponse StackOverflow traite des différences entre les simulacres et les stubs en termes assez simples que vous pouvez lire pour en savoir plus.

 // you can mock concrete classes, not only interfaces LinkedList mockedList = mock(LinkedList.class); // stubbing appears before the actual execution when(mockedList.get(0)).thenReturn("first"); // the following prints "first" System.out.println(mockedList.get(0)); // the following prints "null" because get(999) was not stubbed System.out.println(mockedList.get(999));

De plus, avec Mockito, nous pouvons vérifier si une méthode a été appelée :

 // mock creation List mockedList = mock(List.class); // using mock object - it does not throw any "unexpected interaction" exception mockedList.add("one"); mockedList.clear(); // selective, explicit, highly readable verification verify(mockedList).add("one"); verify(mockedList).clear(); 

Android de test

Maintenant, nous savons que nous pouvons spécifier des paires action-réaction qui définissent ce qui se passe une fois que nous exécutons une action spécifique sur l'objet/composant simulé. Par conséquent, nous pouvons simuler des modules entiers de notre application, et pour chaque cas de test, faire réagir le module simulé d'une manière différente. Les différentes manières refléteront les états possibles du composant testé et de la paire de composants simulés.

Tests unitaires

Dans cette section, nous supposerons l'architecture MVP (Model View Presenter). Les activités et les fragments sont les vues, les modèles étant la couche de référentiel pour les appels à la base de données ou aux services distants, et le présentateur étant le « cerveau » qui relie tout cela en mettant en œuvre une logique spécifique pour contrôler les vues, les modèles et le flux de données à travers le application.

Composants d'abstraction

Vues et modèles moqueurs

Dans cet exemple de test Android, nous simulerons des vues, des modèles et des composants de référentiel, et nous testerons unitairement le présentateur. C'est l'un des plus petits tests, ciblant un seul composant de l'architecture. De plus, nous utiliserons le stub de méthode pour mettre en place une chaîne de réactions appropriée et testable :

 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterTest { private Calendar cal = Calendar.getInstance(); @Mock private IFitnessListModel model; @Mock private IFitnessListView view; private IFitnessListPresenter presenter; @Before public void setup() { MockitoAnnotations.initMocks(this); final FitnessEntry entryMock = mock(FitnessEntry.class); presenter = new FitnessListPresenter(view, model); /* Define the desired behaviour. Queuing the action in "doAnswer" for "when" is executed. Clear and synchronous way of setting reactions for actions (stubbing). */ doAnswer((new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ArrayList<FitnessEntry> items = new ArrayList<>(); items.add(entryMock); ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); return null; } })).when(model).fetchAllItems((IFitnessListPresenterCallback) presenter); } /** Verify if model.fetchItems was called once. Verify if view.onFetchSuccess is called once with the specified list of type FitnessEntry The concrete implementation of ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); calls the view.onFetchSuccess(...) method. This is why we verify that view.onFetchSuccess is called once. */ @Test public void testFetchAll() { presenter.fetchAllItems(false); // verify can be called only on mock objects verify(model, times(1)).fetchAllItems((IFitnessListPresenterCallback) presenter); verify(view, times(1)).onFetchSuccess(new ArrayList<>(anyListOf(FitnessEntry.class))); } }

Se moquer de la couche réseau globale avec MockWebServer

Il est souvent pratique de pouvoir se moquer de la couche réseau globale. MockWebServer nous permet de mettre en file d'attente des réponses pour des requêtes spécifiques que nous exécutons dans nos tests. Cela nous donne la possibilité de simuler des réponses obscures que nous attendons du serveur, mais qui ne sont pas simples à reproduire. Cela nous permet d'assurer une couverture complète tout en écrivant peu de code supplémentaire.

Le référentiel de code de MockWebServer fournit un exemple intéressant auquel vous pouvez vous référer pour une meilleure compréhension de cette bibliothèque.

Doubles de test personnalisés

Vous pouvez écrire votre propre composant de modèle ou de référentiel et l'injecter dans le test en fournissant un module différent au graphe d'objets à l'aide de Dagger (http://square.github.io/dagger/). Nous avons la possibilité de vérifier si l'état de la vue a été correctement mis à jour en fonction des données fournies par le composant de modèle simulé :

 /** Custom mock model class */ public class FitnessListErrorTestModel extends FitnessListModel { // ... @Override public void fetchAllItems(IFitnessListPresenterCallback callback) { callback.onError(); } @Override public void fetchItemsInRange(final IFitnessListPresenterCallback callback, DateFilter filter) { callback.onError(); } }
 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterDaggerTest { private FitnessActivity activity; private FitnessListFragment fitnessListFragment; @Before public void setup() { /* setupActivity runs the Activity lifecycle methods on the specified class */ activity = Robolectric.setupActivity(FitnessActivity.class); fitnessListFragment = activity.getFitnessListFragment(); /* Create the objectGraph with the TestModule */ ObjectGraph localGraph = ObjectGraph.create(TestModule.newInstance(fitnessListFragment)); /* Injection */ localGraph.inject(fitnessListFragment); localGraph.inject(fitnessListFragment.getPresenter()); } @Test public void testInteractorError() { fitnessListFragment.getPresenter().fetchAllItems(false); /* suppose that our view shows a Toast message with the specified text below when an error is reported, so we check for it. */ assertEquals(ShadowToast.getTextOfLatestToast(), "Something went wrong!"); } @Module( injects = { FitnessListFragment.class, FitnessListPresenter.class }, overrides = true, library = true ) static class TestModule { private IFitnessListView view; private TestModule(IFitnessListView view){ this.view = view; } public static TestModule newInstance(IFitnessListView view){ return new TestModule(view); } @Provides public IFitnessListInteractor provideFitnessListInteractor(){ return new FitnessListErrorTestModel(); } @Provides public IFitnessListPresenter provideFitnessPresenter(){ return new FitnessListPresenter(view); } } }

Tests en cours

Studio Android

Vous pouvez facilement cliquer avec le bouton droit sur une classe de test, une méthode ou un package de test complet et exécuter les tests à partir de la boîte de dialogue des options de l'IDE.

Terminal

L'exécution de tests d'applications Android à partir du terminal crée des rapports pour les classes testées dans le dossier "build" du module cible. De plus, si vous envisagez de configurer un processus de construction automatisé, vous utiliserez l'approche terminale. Avec Gradle, vous pouvez exécuter tous les tests de débogage en exécutant ce qui suit :

 gradle testDebug

Accéder au jeu de sources "test" à partir de la version Android Studio

La version 1.1 d'Android Studio et du plugin Android Gradle apporte la prise en charge des tests unitaires de votre code. Vous pouvez en savoir plus en lisant leur excellente documentation à ce sujet. La fonctionnalité est expérimentale, mais également une excellente inclusion puisque vous pouvez désormais basculer facilement entre vos tests unitaires et vos ensembles de sources de test d'instrumentation à partir de l'IDE. Il se comporte de la même manière que si vous changez de version dans l'IDE.

Tests unitaires Android

Faciliter le processus

L'écriture de tests d'applications Android n'est peut-être pas aussi amusante que le développement de l'application d'origine. Par conséquent, quelques conseils sur la façon de faciliter le processus d'écriture des tests et d'éviter les problèmes courants lors de la configuration du projet vous aideront grandement.

AssertJ Android

AssertJ Android, comme vous l'avez peut-être deviné d'après son nom, est un ensemble de fonctions d'assistance conçues pour Android. C'est une extension de la bibliothèque populaire AssertJ. Les fonctionnalités fournies par AssertJ Android vont des assertions simples, telles que "assertThat(view).isGone()", à des choses aussi complexes que :

 assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE)

Avec AssertJ Android et son extensibilité, vous avez la garantie d'un bon point de départ simple pour écrire des tests pour les applications Android.

Chemin robotique et manifeste

Lors de l'utilisation de Robolectric, vous remarquerez peut-être que vous devez spécifier l'emplacement du manifeste et que la version du SDK est définie sur 18. Vous pouvez le faire en incluant une annotation "Config".

 @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18)

L'exécution de tests nécessitant Robolectric à partir du terminal peut introduire de nouveaux défis. Par exemple, vous pouvez voir des exceptions telles que "Thème non défini". Si les tests s'exécutent correctement depuis l'IDE, mais pas depuis le terminal, vous essayez peut-être de l'exécuter à partir d'un chemin dans le terminal où le chemin du manifeste spécifié ne peut pas être résolu. La valeur de configuration codée en dur pour le chemin du manifeste peut ne pas pointer vers le bon emplacement à partir du point d'exécution de la commande. Cela peut être résolu grâce à l'utilisation de coureurs personnalisés :

 public class RobolectricGradleTestRunner extends RobolectricTestRunner { public RobolectricGradleTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override protected AndroidManifest getAppManifest(Config config) { String appRoot = "../app/src/main/"; String manifestPath = appRoot + "AndroidManifest.xml"; String resDir = appRoot + "res"; String assetsDir = appRoot + "assets"; AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)); return manifest; } }

Configuration progressive

Vous pouvez utiliser ce qui suit pour configurer Gradle pour les tests unitaires. Vous devrez peut-être modifier les noms de dépendance et les versions requises en fonction des besoins de votre projet.

 // Robolectric testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' testCompile 'com.squareup.dagger:dagger:1.2.2' testProvided 'com.squareup.dagger:dagger-compiler:1.2.2' testCompile 'com.android.support:support-v4:21.0.+' testCompile 'com.android.support:appcompat-v7:21.0.3' testCompile('org.robolectric:robolectric:2.4') { exclude module: 'classworlds' exclude module: 'commons-logging' exclude module: 'httpclient' exclude module: 'maven-artifact' exclude module: 'maven-artifact-manager' exclude module: 'maven-error-diagnostics' exclude module: 'maven-model' exclude module: 'maven-project' exclude module: 'maven-settings' exclude module: 'plexus-container-default' exclude module: 'plexus-interpolation' exclude module: 'plexus-utils' exclude module: 'wagon-file' exclude module: 'wagon-http-lightweight' exclude module: 'wagon-provider-api' }

Services de robotique et de jeu

Si vous utilisez Google Play Services, vous devrez créer votre propre constante entière pour la version Play Services afin que Robolectric fonctionne correctement dans cette configuration d'application.

 <meta-data android:name="com.google.android.gms.version" android:value="@integer/gms_version" tools:replace="android:value" />

Dépendances Robolectric pour prendre en charge les bibliothèques

Un autre problème de test intéressant est que Robolectric n'est pas en mesure de référencer correctement les bibliothèques de support. La solution est d'ajouter un fichier "project.properties" au module où se trouvent les tests. Par exemple, pour les bibliothèques Support-v4 et AppCompat, le fichier doit contenir :

 android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3 android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3

Tests d'acceptation/de régression

Les tests d'acceptation/régression automatisent une partie de l'étape finale des tests sur un environnement réel 100 % Android. Nous n'utilisons pas de classes de système d'exploitation Android simulées à ce niveau - les tests sont exécutés sur de vrais appareils et émulateurs.

tests d'acceptation et de régression Android

Ces circonstances rendent le processus beaucoup plus instable en raison de la variété des périphériques physiques, des configurations d'émulateurs, des états des périphériques et des ensembles de fonctionnalités de chaque périphérique. De plus, cela dépend fortement de la version du système d'exploitation et de la taille de l'écran du téléphone pour décider comment le contenu sera affiché.

Il est un peu complexe de créer le bon test qui passe sur une large gamme d'appareils, mais comme toujours, vous devez rêver grand mais commencer petit. La création de tests avec Robotium est un processus itératif. Avec quelques astuces, cela peut être beaucoup simplifié.

Robotium

Robotium est un framework d'automatisation de test Android open source qui existe depuis janvier 2010. Il convient de mentionner que Robotium est une solution payante, mais est livrée avec un essai gratuit équitable.

Pour accélérer le processus d'écriture des tests Robotium, nous allons passer de l'écriture manuelle des tests à l'enregistrement des tests. Le compromis est entre la qualité du code et la vitesse. Si vous apportez de lourdes modifications à votre interface utilisateur, vous bénéficierez grandement de l'approche d'enregistrement des tests et pourrez enregistrer rapidement de nouveaux tests.

Testdroid Recorder est un enregistreur de test gratuit qui crée des tests Robotium en enregistrant les clics que vous effectuez sur l'interface utilisateur. L'installation de l'outil est super facile, comme décrit dans leurs documentations accompagnées d'une vidéo étape par étape.

Étant donné que Testdroid Recorder est un plugin Eclipse et que nous faisons référence à Android Studio tout au long de cet article, ce serait idéalement un motif de préoccupation. Cependant, dans ce cas, ce n'est pas un problème, car vous pouvez utiliser le plugin directement avec un APK et enregistrer les tests par rapport à celui-ci.

Une fois que vous avez créé les tests, vous pouvez les copier et les coller dans Android Studio, ainsi que toute dépendance requise par l'enregistreur Testdroid, et vous êtes prêt à partir. Le test enregistré ressemblerait à la classe ci-dessous :

 public class LoginTest extends ActivityInstrumentationTestCase2<Activity> { private static final String LAUNCHER_ACTIVITY_CLASSNAME = "com.toptal.fitnesstracker.view.activity.SplashActivity"; private static Class<?> launchActivityClass; static { try { launchActivityClass = Class.forName(LAUNCHER_ACTIVITY_CLASSNAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } private ExtSolo solo; @SuppressWarnings("unchecked") public LoginTest() { super((Class<Activity>) launchActivityClass); } // executed before every test method @Override public void setUp() throws Exception { super.setUp(); solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass() .getCanonicalName(), getName()); } // executed after every test method @Override public void tearDown() throws Exception { solo.finishOpenedActivities(); solo.tearDown(); super.tearDown(); } public void testRecorded() throws Exception { try { assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_username_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_username_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_username_input"), "[email protected]"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_password_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_password_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_password_input"), "123456"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); solo.clickOnButton((Button) solo .findViewById("com.toptal.fitnesstracker.R.id.parse_login_button")); assertTrue("Wait for text fitness list activity.", solo.waitForActivity(FitnessActivity.class)); assertTrue("Wait for text KM.", solo.waitForText("KM", 20000)); /* Custom class that enables proper clicking of ActionBar action items */ TestUtils.customClickOnView(solo, R.id.action_logout); solo.waitForDialogToOpen(); solo.waitForText("OK"); solo.clickOnText("OK"); assertTrue("waiting for ParseLoginActivity after logout", solo.waitForActivity(ParseLoginActivity.class)); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); } catch (AssertionFailedError e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } catch (Exception e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } } }

Si vous regardez attentivement, vous remarquerez à quel point le code est plutôt simple.

Lors de l'enregistrement des tests, ne vous limitez pas aux déclarations « attendre ». Attendez que les boîtes de dialogue apparaissent, que les activités apparaissent, que les textes apparaissent. Cela garantira que l'activité et la hiérarchie des vues sont prêtes à interagir lorsque vous effectuez l'action sur l'écran actuel. En même temps, faites des captures d'écran. Les tests automatisés sont généralement sans surveillance et les captures d'écran sont l'un des moyens de voir ce qui s'est réellement passé pendant ces tests.

Que les tests réussissent ou échouent, les rapports sont votre meilleur ami. Vous pouvez les trouver sous le répertoire de construction "module/build/outputs/reports":

rapport d'essai

En théorie, l'équipe QA pourrait enregistrer les tests et les optimiser. En mettant l'effort dans un modèle standardisé pour optimiser les cas de test, cela pourrait être fait. Lorsque vous enregistrez normalement des tests, vous devez toujours modifier quelques éléments pour que cela fonctionne parfaitement.

Enfin, pour exécuter ces tests depuis Android Studio, vous pouvez les sélectionner et les exécuter comme vous le feriez pour des tests unitaires. Depuis le terminal, c'est un one-liner :

 gradle connectedAndroidTest

Performance des tests

Les tests unitaires Android avec Robolectric sont extrêmement rapides, car ils s'exécutent directement dans la JVM de votre machine. Par rapport à cela, les tests d'acceptation sur les émulateurs et les appareils physiques sont beaucoup plus lents. Selon la taille des flux que vous testez, cela peut prendre de quelques secondes à quelques minutes par scénario de test. La phase de test d'acceptation doit être utilisée dans le cadre d'un processus de génération automatisé sur un serveur d'intégration continue.

La vitesse peut être améliorée par la parallélisation sur plusieurs appareils. Découvrez cet excellent outil de Jake Wharton et les gars de Square http://square.github.io/spoon/. Il y a aussi de beaux reportages.

Les plats à emporter

Il existe une variété d'outils de test Android disponibles, et à mesure que l'écosystème mûrit, le processus de mise en place d'un environnement testable et d'écriture de tests deviendra plus facile. Il y a encore plus de défis à relever, et avec une large communauté de développeurs travaillant sur des problèmes quotidiens, il y a beaucoup de place pour des discussions constructives et des retours rapides.

Utilisez les approches décrites dans ce didacticiel de test Android pour vous guider dans la résolution des défis qui vous attendent. Si et quand vous rencontrez des problèmes, consultez cet article ou les références liées pour trouver des solutions aux problèmes connus.

Dans un prochain article, nous discuterons de la parallélisation, de l'automatisation de la construction, de l'intégration continue, des crochets Github/BitBucket, de la gestion des versions d'artefacts et des meilleures pratiques pour gérer plus en profondeur des projets d'applications mobiles massifs.