Fermer

juillet 25, 2018

Comment améliorer la couverture de test pour votre application Android en utilisant Mockito et Espresso


À propos de l'auteur

Vivek est ingénieur chez Zeta.
En savoir plus sur Vivek

Les cadres tels que Espresso et Mockito fournissent des API faciles à utiliser qui facilitent l'écriture de tests pour différents scénarios. Couvrons les fondamentaux des tests et des frameworks que les développeurs peuvent utiliser pour écrire des tests unitaires.

Dans le développement d'applications, divers cas d'utilisation et interactions apparaissent au fur et à mesure des itérations du code. L'application peut devoir extraire des données d'un serveur, interagir avec les capteurs du périphérique, accéder au stockage local ou rendre des interfaces utilisateur complexes.

La principale chose à prendre en compte lors de la rédaction des tests est la conception des nouvelles fonctionnalités. . Le test unitaire devrait couvrir toutes les interactions possibles avec l'unité, y compris les interactions standard et les scénarios exceptionnels.

Dans cet article, nous aborderons les principes fondamentaux des tests et des frameworks tels que Mockito et Espresso, que les développeurs peuvent utiliser pour écrire des tests unitaires. Je vais également discuter brièvement de la manière d'écrire du code testable. J'expliquerai également comment démarrer avec des tests locaux et instrumentés dans Android.

Lecture recommandée : Comment configurer un système de test automatisé à l'aide de téléphones Android (étude de cas) [19659009] Fondamentaux du test

Un test unitaire typique comporte trois phases.

  1. Premièrement, le test unitaire initialise une petite partie d'une application à tester.
  2. Ensuite, il applique un stimulus au système testé,
  3. Enfin, il observe le comportement résultant.

Si le comportement observé est conforme aux attentes, le test unitaire réussit; sinon, cela échoue, indiquant qu'il y a un problème quelque part dans le système testé. Ces trois phases de test unitaires sont également connues sous les noms a ct et a ssert ou simplement AAA. L'application devrait idéalement comprendre trois catégories de tests: petit, moyen et grand.

Note: Un test d'instrumentation est un type de test d'intégration. Ce sont des tests qui s'exécutent sur un périphérique ou un émulateur Android. Ces tests ont accès à des informations sur l’instrumentation, telles que le contexte de l’application testée. Utilisez cette approche pour exécuter des tests unitaires avec des dépendances Android que les objets fictifs ne peuvent pas facilement satisfaire.

L'écriture de petits tests vous permet de résoudre rapidement les problèmes, mais il est difficile de savoir qu'un test réussi permettra à votre application de fonctionner. Il est important d'avoir des tests de toutes les catégories dans l'application, bien que la proportion de chaque catégorie puisse varier d'une application à l'autre. Un bon test unitaire devrait être facile à écrire lisible fiable et rapide .

Voici une brève introduction à Mockito et Espresso, qui facilite les tests des applications Android.

Mockito

Il existe différents cadres de moquerie, mais le plus populaire est Mockito :

Mockito est un cadre moqueur qui a vraiment du goût . Il vous permet d'écrire de beaux tests avec une API propre et simple. Mockito ne vous donne pas la gueule de bois parce que les tests sont très lisibles et qu'ils génèrent des erreurs de vérification propres.

Son API fluide sépare la préparation pré-test de la validation post-test. Si le test échoue, Mockito indique clairement où nos attentes diffèrent de la réalité! La bibliothèque contient tout ce dont vous avez besoin pour écrire des tests complets.

Espresso

Espresso vous aide à rédiger des tests d'interface utilisateur Android concis, beaux et fiables.

Le code ci-dessous montre un exemple de test Espresso. Nous reprendrons le même exemple plus loin dans ce tutoriel lorsque nous parlerons en détail des tests d'instrumentation.

 @Test
public void setUserName () {
    onView (withId (R.id.name_field)). perform (typeText ("Vivek Maskara"));
    onView (withId (R.id.set_user_name)). perform (click ());
    onView (withText ("Hello Vivek Maskara!")). check (correspond à (isDisplayed ()));
}

Espresso teste clairement les attentes, les interactions et les assertions, sans se laisser distraire par un contenu standard, une infrastructure personnalisée ou des détails de mise en œuvre compliqués. Chaque fois que votre test invoque onView () Espresso attend pour effectuer l'action ou l'assertion d'interface utilisateur correspondante jusqu'à ce que les conditions de synchronisation soient satisfaites, à savoir:

  • la file d'attente est vide,
  • ] AsyncTask exécute actuellement une tâche,
  • les ressources inactives sont inactives.

Ces vérifications garantissent la fiabilité des résultats de test.

Rédaction de code testable

impossible. Un bon design et seulement une bonne conception peuvent faciliter les tests unitaires. Voici quelques-uns des concepts importants pour l’écriture de code testable

Évitez de mélanger la construction de graphes d’objet avec la logique d’application

Dans un test, vous voulez instancier la classe testée et appliquer un stimulus à la classe. le comportement attendu a été observé. Assurez-vous que la classe testée n'instancie pas d'autres objets et que ces objets n'instancient pas plus d'objets, etc. Pour avoir une base de code testable, votre application doit avoir deux types de classes:

  • Les fabriques, qui sont pleines des "nouveaux" opérateurs et qui sont responsables de la construction du graphe d'objet de votre application;

Les constructeurs ne doivent faire aucun travail

L'opération la plus courante que vous effectuerez dans les tests est l'instanciation des graphes d'objet. Donc, facilitez-vous la tâche et faites en sorte que les constructeurs ne fassent rien d'autre que d'affecter toutes les dépendances aux champs. Faire le travail dans le constructeur non seulement affectera les tests directs de la classe, mais affectera également les tests liés qui tentent d’instancier indirectement votre classe.

Eviter les méthodes statiques dans la mesure du possible

La clé des tests est la présence de lieux où vous pouvez détourner le flux d'exécution normal. Des coutures sont nécessaires pour isoler l'unité de test. Si vous créez une application avec uniquement des méthodes statiques, vous aurez une application procédurale. Le degré de souffrance d’une méthode statique du point de vue des tests dépend de l’endroit où elle se trouve dans le graphe d’appel de votre application. Une méthode feuille telle que Math.abs () ne pose pas de problème car le graphe d'appel d'exécution se termine là. Mais si vous choisissez une méthode dans un noyau de votre logique applicative, alors tout ce qui se trouve derrière la méthode sera difficile à tester, car il n'y a aucun moyen d'insérer des doubles de test.

Évitez de mélanger les préoccupations

traiter avec une seule entité. Dans une classe, une méthode devrait être responsable de faire une seule chose. Par exemple, BusinessService devrait être le seul à avoir parlé à un Business et non à un BusinessReceipts . De plus, un procédé dans BusinessService pourrait être getBusinessProfile mais une méthode telle que createAndGetBusinessProfile ne serait pas idéale pour les tests. S OLID les principes de conception doivent être respectés pour une bonne conception:

  • S : principe de responsabilité unique;
  • O : ouvert-fermé principe;
  • L : Principe de substitution de Liskov;
  • I : principe de ségrégation d'interface;
  • D : principe d'inversion de dépendance.

Dans les sections suivantes, nous utiliserons des exemples issus d'une application très simple que j'ai construite pour ce tutoriel. L'application dispose d'un EditText qui prend un nom d'utilisateur en entrée et affiche le nom dans un TextView sur un clic de bouton. N'hésitez pas à prendre le code source complet pour le projet de GitHub. Voici une capture d'écran de l'application:


 Exemple de test
Grand aperçu

Rédaction des tests d'unité locale

Les tests unitaires peuvent être exécutés localement sur votre machine de développement sans périphérique ni émulateur. Cette approche de test est efficace car elle évite d'avoir à charger l'application cible et le code de test unitaire sur un périphérique physique ou un émulateur à chaque exécution de votre test. En plus de Mockito, vous devrez également configurer les dépendances de test pour que votre projet utilise les API standard fournies par le framework JUnit 4.

Configuration de l'environnement de développement

Commencez par ajouter une dépendance à JUnit4 dans votre projet. . La dépendance est du type testImplementation ce qui signifie que les dépendances ne sont nécessaires que pour compiler la source de test du projet.

 testImplementation 'junit: junit: 4.12'

Nous aurons également besoin de la bibliothèque Mockito pour faciliter les interactions avec les dépendances Android.

 testImplementation "org.mockito: mockito-core: $ MOCKITO_VERSION" 

Assurez-vous de synchroniser le projet après avoir ajouté la dépendance. Android Studio devrait avoir créé la structure de dossiers pour les tests unitaires par défaut. Sinon, assurez-vous que la structure de répertoires suivante existe:

 / app / src / test / java / com / maskaravivek / testingExamples

Création de votre premier test unitaire

Supposons que vous vouliez tester la fonction displayUserName dans le UserService . Par souci de simplicité, la fonction formate simplement l'entrée et la renvoie. Dans une application du monde réel, il pourrait effectuer un appel réseau pour récupérer le profil utilisateur et renvoyer le nom de l'utilisateur.

 @Singleton
classe UserService @Inject
constructeur (contexte var privé: contexte) {
    
    fun displayUserName (name: String): String {
        val userNameFormat = context.getString (R.string.display_user_name)
        return String.format (Locale.ENGLISH, userNameFormat, name)
    }
}

Nous allons commencer par créer une classe UserServiceTest dans notre répertoire de test. La classe UserService utilise le contexte qui doit être simulé à des fins de test. Mockito fournit une notation @Mock pour les objets moqueurs, qui peut être utilisée comme suit:

 @Mock internal context context: Context? = null

De même, vous devrez simuler toutes les dépendances requises pour construire l'instance de la classe UserService . Avant votre test, vous devez initialiser ces simulacres et les injecter dans la classe UserService .

  • @InjectMock crée une instance de la classe et injecte les simulations marquées avec les annotations @Mock dedans.
  • MockitoAnnotations.initMocks (this); initialise les champs annotés avec des annotations Mockito.

Voici comment faire: [19659035] classe UserServiceTest {
    
    @Mock interne du contexte var: contexte? = null
    @InjectMocks interne var userService: UserService? = null
    
    @Avant
    installation amusante () {
        MockitoAnnotations.initMocks (this)
    }
}

Vous avez maintenant terminé la configuration de votre classe de test. Ajoutons un test à cette classe qui vérifie la fonctionnalité de la fonction displayUserName . Voici à quoi ressemble le test:

 @Test
fun displayUserName () {
    doReturn ("Hello% s!"). `when` (context) !!. getString (any (Int :: class.java))
    val displayUserName = userService !!. displayUserName ("Test")
    assertEquals (displayUserName, "Hello Test!")
}

Le test utilise une instruction doReturn (). When () pour fournir une réponse lorsqu'un appel context.getString () est effectué. Pour tout entier d'entrée, le résultat sera le même, "Hello% s!" . Nous aurions pu être plus précis en le faisant renvoyer cette réponse uniquement pour un ID de ressource de chaîne particulier, mais pour des raisons de simplicité, nous renvoyons la même réponse à n'importe quelle entrée.
Enfin, voici à quoi ressemble la classe de test:

 classe UserServiceTest {
    @Mock interne du contexte var: contexte? = null
    @InjectMocks interne var userService: UserService? = null
    @Avant
    installation amusante () {
        MockitoAnnotations.initMocks (this)
    }
     
    @Tester
    fun displayUserName () {
        doReturn ("Hello% s!"). `when` (context) !!. getString (any (Int :: class.java))
        val displayUserName = userService !!. displayUserName ("Test")
        assertEquals (displayUserName, "Hello Test!")
    }
}

Exécution de vos tests unitaires

Pour exécuter les tests unitaires, vous devez vous assurer que Gradle est synchronisé. Pour exécuter un test, cliquez sur l'icône de lecture verte dans l'EDI.


en vous assurant que Gradle est synchronisé

Lorsque les tests unitaires sont exécutés, avec succès ou non, vous devriez voir cela dans le menu "Exécuter" en bas de l'écran:

 résultat après l'exécution des tests unitaires
Grand aperçu [19659089] Vous avez terminé votre premier test unitaire!

Ecriture des tests d'instrumentation

Les tests d'instrumentation sont les plus adaptés pour vérifier les valeurs des composants de l'interface utilisateur lors de l'exécution d'une activité. Par exemple, dans l'exemple ci-dessus, nous voulons nous assurer que TextView affiche le nom d'utilisateur correct après avoir cliqué sur le bouton . Ils s'exécutent sur des périphériques physiques et des émulateurs et peuvent tirer parti des API du framework Android et des API de prise en charge, telles que la bibliothèque de support des tests Android.
Nous utiliserons Espresso pour effectuer des actions sur le thread principal, telles que les clics de bouton et les modifications de texte.

Configuration de l'environnement de développement

Ajoutez une dépendance à Espresso:

 .espresso: espresso-core: 3.0.1 '

Les tests d'instrumentation sont créés dans un dossier androidTest .

  / app / src / androidTest / java / com / maskaravivek / testingExamples

Si vous souhaitez tester une activité simple, créez votre classe de test dans le même package que votre activité.

Création de votre premier test d'instrumentation

Commençons par créer une activité simple qui prend un nom en entrée et sur le clic d'un bouton, affiche le nom d'utilisateur. Le code pour l'activité ci-dessus est assez simple:

 class MainActivity: AppCompatActivity () {
    
    bouton var: bouton? = null
    var userNameField: EditText? = null
    var displayUserName: TextView? = null
    
    remplacer fun fun onCreate (savedInstanceState: Bundle?) {
        super.onCreate (savedInstanceState)
        AndroidInjection.inject (this)
        setContentView (R.layout.activity_main)
        initViews ()
    }
    
    fun initViews privé () {
        button = this.findViewById (R.id.set_user_name)
        userNameField = this.findViewById (R.id.name_field)
        displayUserName = this.findViewById (R.id.display_user_name)
    
        this.button !!. setOnClickListener ({
            displayUserName !!. text = "Bonjour $ {userNameField !!. text}!"
        })
    }
}

Pour créer un test pour la MainActivity nous allons commencer par créer une classe MainActivityTest sous le répertoire androidTest . Ajoutez l'annotation AndroidJUnit4 à la classe pour indiquer que les tests de cette classe utiliseront la classe de testeur Android par défaut.

 @RunWith (AndroidJUnit4 :: class)
classe MainActivityTest {}

Ensuite, ajoutez un ActivityTestRule à la classe. Cette règle fournit des tests fonctionnels d'une seule activité. Pendant la durée du test, vous pourrez manipuler directement votre activité en utilisant la référence obtenue à partir de getActivity () .

 @Rule @JvmField var activityActivityTestRule = ActivityTestRule (MainActivity :: class.java)

Maintenant que vous avez terminé de configurer la classe de test, ajoutons un test qui vérifie que le nom d'utilisateur est affiché en cliquant sur le bouton "Définir le nom d'utilisateur".

 @Test
fun setUserName () {
    onView (withId (R.id.name_field)). perform (typeText ("Vivek Maskara"))
    onView (withId (R.id.set_user_name)). Effectuez (click ())
    onView (withText ("Hello Vivek Maskara!")). check (correspond à (isDisplayed ()))
}

Le test ci-dessus est assez simple à suivre. Il simule tout d'abord du texte tapé dans le EditText effectue l'action de clic sur le bouton, puis vérifie si le texte correct est affiché dans le TextView .

Le test final la classe ressemble à ceci:

 @RunWith (AndroidJUnit4 :: class)
classe MainActivityTest {
    
    @Rule @JvmField var activityActivityTestRule = ActivityTestRule (MainActivity :: class.java)
    
    @Tester
    fun setUserName () {
        onView (withId (R.id.name_field)). perform (typeText ("Vivek Maskara"))
        onView (withId (R.id.set_user_name)). Effectuez (click ())
        onView (withText ("Hello Vivek Maskara!")). check (correspond à (isDisplayed ()))
    }
}

Exécution de vos tests d'instrumentation

Comme pour les tests unitaires, cliquez sur le bouton de lecture vert dans l'EDI pour exécuter le test.


 Cliquez sur le bouton de lecture vert dans IDE pour lancer le test

Après un clic sur le bouton de lecture, la version de test de l'application sera installée sur l'émulateur ou le périphérique, et le test s'exécutera automatiquement.


Grand aperçu

Test d'instrumentation avec Dagger, Mockito, Et Espresso

Espresso est l'un des frameworks de test les plus populaires, avec une bonne documentation et un support communautaire. Mockito s'assure que les objets effectuent les actions attendues d'eux. Mockito fonctionne également bien avec les bibliothèques d'injection de dépendances telles que Dagger. Le fait de se moquer des dépendances nous permet de tester un scénario de manière isolée.
Jusqu'à présent, notre MainActivity n'a utilisé aucune injection de dépendance et, par conséquent, nous avons pu écrire très facilement notre test d'interface utilisateur. Pour rendre les choses un peu plus intéressantes, injectons UserService dans la MainActivity et utilisons-le pour afficher le texte.

 class MainActivity: AppCompatActivity () {
    
    bouton var: bouton? = null
    var userNameField: EditText? = null
    var displayUserName: TextView? = null
    
    @Injecter tardivement var userService: UserService
    
    remplacer fun fun onCreate (savedInstanceState: Bundle?) {
        super.onCreate (savedInstanceState)
        AndroidInjection.inject (this)
        setContentView (R.layout.activity_main)
        initViews ()
    }
    
    fun initViews privé () {
        button = this.findViewById (R.id.set_user_name)
        userNameField = this.findViewById (R.id.name_field)
        displayUserName = this.findViewById (R.id.display_user_name)
    
        this.button !!. setOnClickListener ({
            displayUserName !!. text = userService.displayUserName (userNameField !!. text.toString ())
        })
    }
}

Avec Dagger sur la photo, nous devrons mettre en place quelques éléments avant d’écrire des tests d’instrumentation.
Imaginez que la fonction displayUserName utilise en interne certaines API pour extraire les détails de l'utilisateur. Il ne devrait pas y avoir de situation dans laquelle un test ne passe pas en raison d’une erreur du serveur. Pour éviter une telle situation, nous pouvons utiliser la structure d'injection de dépendance Dagger et, pour la mise en réseau, la mise à niveau.

Configuration de la dague dans l'application

Nous allons rapidement configurer les modules et composants de base requis pour Dagger. Si tu n'es pas
familier avec Dagger, consultez la documentation de Google à ce sujet. Nous allons commencer à ajouter des dépendances pour l'utilisation de Dagger dans le fichier build.gradle .

 implementation "com.google.dagger: dagger-android: $ DAGGER_VERSION"
implémentation "com.google.dagger: dagger-android-support: $ DAGGER_VERSION"
implémentation "com.google.dagger: dagger: $ DAGGER_VERSION"
kapt "com.google.dagger: dagger-compiler: $ DAGGER_VERSION"
kapt "com.google.dagger: dagger-android-processor: $ DAGGER_VERSION"

Créez un composant dans la classe Application et ajoutez les modules nécessaires qui seront utilisés dans notre projet. Nous devons injecter des dépendances dans la MainActivity de notre application. Nous ajouterons un @Module pour l'injection dans l'activité.

 @Module
Classe abstraite ActivityBuilder {
    @ContributsAndroidInjector
    fun abstrait interne bindMainActivity (): MainActivity
}

La classe AppModule fournira les différentes dépendances requises par l'application. Pour notre exemple, il fournira simplement une instance de Context et UserService .

 @Module
classe ouverte AppModule (application val: Application) {
    @Provides
    @Singleton
    fun open interne provideContext (): Context {
        demande de retour
    }
    
    @Provides
    @Singleton
    fun open interne provideUserService (context: Context): UserService {
        retourne UserService (context)
    }
}

La classe AppComponent vous permet de générer le graphe d'objet pour l'application.

 @Singleton
@Component (modules = [(AndroidSupportInjectionModule::class), (AppModule::class), (ActivityBuilder::class)])
interface AppComponent {
    
    @ Component.Builder
    Interface Builder {
        appModule amusant (appModule: AppModule): Générateur
        fun build (): AppComponent
    }
    
    fun inject (application: ExamplesApplication)
}

Créez une méthode qui renvoie le composant déjà créé, puis injectez ce composant dans onCreate () .

 open class ExamplesApplication: Application (), HasActivityInjector {
    @Inject lateinit var dispatchingActivityInjector: DispatchingAndroidInjector 
    
    remplacer fun onCreate () {
        super.onCreate ()
        initAppComponent (). inject (this)
    }
    
    open fun initAppComponent (): AppComponent {
        retourne DaggerAppComponent
            .constructeur()
            .appModule (AppModule (this))
            .construire()
    }
    
    outrepasser amusement activityInjector (): DispatchingAndroidInjector ? {
        return dispatchingActivityInjector
    }
}

Configuration de la dague dans l'application de test

Pour simuler les réponses du serveur, nous devons créer une nouvelle classe Application qui étend la classe ci-dessus.

 class TestExamplesApplication: ExamplesApplication ( ) {
    
    substitut fun initAppComponent (): AppComponent {
        retourne DaggerAppComponent.builder ()
            .appModule (MockApplicationModule (this))
            .construire()
    }
    
    @Module
    classe interne privée constructeur interne MockApplicationModule (application: Application): AppModule (application) {
        redéfinit fun DivisionUserService (contexte: Contexte): UserService {
            val mock = Mockito.mock (UserService :: class.java)
            `when` (mock !!. displayUserName (" Test ")). thenReturn (" Hello Test! ")
            retour de simulacre
        }
    }
}

Comme vous pouvez le voir dans l'exemple ci-dessus, nous avons utilisé Mockito pour simuler UserService et en assumer les résultats. Nous avons toujours besoin d'un nouveau programme d'exécution qui pointe vers la nouvelle classe d'application avec les données écrasées.

 classe MockTestRunner: AndroidJUnitRunner () {
    
    remplacer fun onCreate (arguments: Bundle) {
        StrictMode.setThreadPolicy (StrictMode.ThreadPolicy.Builder (). PermitAll (). Build ())
        super.onCreate (arguments)
    }
    
    @Throws (InstantiationException :: class, IllegalAccessException :: class, ClassNotFoundException :: class)
    redéfinissez fun newApplication (cl: ClassLoader, className: String, context: Context): Application {
        retourne super.newApplication (cl, TestExamplesApplication :: class.java.name, context)
    }
}

Ensuite, vous devez mettre à jour le fichier build.gradle pour utiliser le MockTestRunner .

 android {
    ...
    
    defaultConfig {
        ...
        testInstrumentationRunner ".MockTestRunner"
    }
}

Exécution du test

Tous les tests avec la nouvelle TestExamplesApplication et MockTestRunner doivent être ajoutés au package androidTest . Cette implémentation rend les tests totalement indépendants du serveur et nous permet de manipuler les réponses.
Avec la configuration ci-dessus en place, notre classe de test ne changera pas du tout. Lorsque le test est exécuté, l'application utilisera TestExamplesApplication au lieu de ExamplesApplication et, par conséquent, une instance simulée de UserService sera utilisée.

 @ RunWith (AndroidJUnit4 :: class)
classe MainActivityTest {
    @Rule @JvmField var activityActivityTestRule = ActivityTestRule (MainActivity :: class.java)
    
    @Tester
    fun setUserName () {
        onView (withId (R.id.name_field)). perform (typeText ("Test"))
        onView (withId (R.id.set_user_name)). Effectuez (click ())
        onView (withText ("Hello Test!")). check (correspond à (isDisplayed ()))
    }
}

Le test s'exécutera avec succès lorsque vous cliquerez sur le bouton de lecture vert dans l'EDI.


 Configuration réussie de Dagger et exécution de tests avec Espresso et Mockito
Grand aperçu

C'est ça! Vous avez réussi à configurer Dagger et à exécuter des tests avec Espresso et Mockito.

Conclusion

Nous avons souligné que l'aspect le plus important de l'amélioration de la couverture du code consiste à écrire du code testable. Les frameworks tels que Espresso et Mockito fournissent des API faciles à utiliser qui facilitent l'écriture de tests pour différents scénarios. Les tests doivent être exécutés isolément, et se moquer des dépendances nous permet de nous assurer que les objets exécutent les actions attendues.

Divers outils de test Android sont disponibles et, au fur et à mesure de la croissance de la mise en place d'un environnement testable et les tests d'écriture deviendront plus faciles.

Écrire des tests unitaires requiert de la discipline, de la concentration et des efforts supplémentaires. En créant et en exécutant des tests unitaires sur votre code, vous pouvez facilement vérifier que la logique des unités individuelles est correcte. L'exécution de tests unitaires après chaque génération vous aide à détecter et à corriger rapidement les régressions logicielles introduites par les modifications de code apportées à votre application. Le blog de test de Google présente les avantages de des tests unitaires.
Le code source complet pour les exemples utilisés dans cet article est disponible sur GitHub. N'hésitez pas à y jeter un coup d'œil.

 Smashing Editorial (da, lf, ra, al, il)




Source link