À 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. 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. 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 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. 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 Ces vérifications garantissent la fiabilité des résultats de test. 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 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: 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. 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 traiter avec une seule entité. Dans une classe, une méthode devrait être responsable de faire une seule chose. Par exemple, 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 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. Commencez par ajouter une dépendance à JUnit4 dans votre projet. . La dépendance est du type Nous aurons également besoin de la bibliothèque Mockito pour faciliter les interactions avec les dépendances Android. 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: Supposons que vous vouliez tester la fonction Nous allons commencer par créer une classe De même, vous devrez simuler toutes les dépendances requises pour construire l'instance de la classe Voici comment faire: [19659035] classe UserServiceTest { Vous avez maintenant terminé la configuration de votre classe de test. Ajoutons un test à cette classe qui vérifie la fonctionnalité de la fonction Le test utilise une instruction 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. 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:
Mockito
Espresso
@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 ()));
}
onView ()
Espresso attend pour effectuer l'action ou l'assertion d'interface utilisateur correspondante jusqu'à ce que les conditions de synchronisation soient satisfaites, à savoir:
Rédaction de code testable
Évitez de mélanger la construction de graphes d’objet avec la logique d’application
Les constructeurs ne doivent faire aucun travail
Eviter les méthodes statiques dans la mesure du possible
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
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:
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:
Rédaction des tests d'unité locale
Configuration de l'environnement de développement
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'
testImplementation "org.mockito: mockito-core: $ MOCKITO_VERSION"
/ app / src / test / java / com / maskaravivek / testingExamples
Création de votre premier test unitaire
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)
}
}
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
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.
@Mock interne du contexte var: contexte? = null
@InjectMocks interne var userService: UserService? = null
@Avant
installation amusante () {
MockitoAnnotations.initMocks (this)
}
}
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!")
}
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
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.
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.
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.
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.
Source link