Cet atelier de programmation fait partie du cours "Advanced Android" en langage Kotlin. Vous tirerez pleinement parti de ce cours si vous suivez les ateliers en séquence, mais ce n'est pas obligatoire. Tous les ateliers de programmation du cours sont répertoriés sur la page de destination des ateliers de programmation Android avancés sur Kotlin.
Introduction
Lorsque vous avez implémenté la première fonctionnalité de votre première application, vous avez probablement exécuté le code pour vérifier qu'il fonctionnait comme prévu. Vous avez effectué un test, mais un test manuel. Comme vous avez continué à ajouter et à mettre à jour des fonctionnalités, vous avez probablement continué à exécuter votre code et à vérifier qu'il fonctionne. Cependant, procéder manuellement à chaque fois est fatiguable, sujet aux erreurs et ne évolue pas.
Les ordinateurs sont parfaits pour le scaling et l'automatisation. Par conséquent, les développeurs de petites et grandes entreprises écrivent des tests automatisés qui sont exécutés par logiciel et qui ne vous obligent pas à exécuter manuellement l'application pour vérifier que le code fonctionne.
Pendant cette série d'ateliers de programmation, vous apprendrez à créer une collection de tests (appelée suite de test) pour une application réelle.
Ce premier atelier de programmation porte sur les principes de base des tests sur Android. Il vous explique comment effectuer vos premiers tests et apprendre à tester les LiveData
et les ViewModel
.
Ce que vous devez déjà savoir
Vous devez être au fait:
- Le langage de programmation Kotlin
- Bibliothèques Android Jetpack principales suivantes :
ViewModel
etLiveData
- Architecture d'application, basée sur le modèle du Guide de l'architecture des applications et Ateliers de programmation Android Fundamentals
Points abordés
Vous allez découvrir les sujets suivants:
- Rédiger et exécuter des tests unitaires sur Android
- Utiliser le développement piloté par le test
- Choisir des tests instrumentés et des tests locaux
Vous découvrirez les concepts et bibliothèques suivants:
- JUnit 4
- Hamcrest
- Bibliothèque de tests AndroidX
- Bibliothèque principale des composants d'architecture AndroidX
Objectifs de l'atelier
- Configurez, exécutez et interprétez des tests locaux et instrumentés sur Android.
- Écrivez des tests unitaires sur Android à l'aide de JUnit4 et Hamcrest.
- Rédigez des tests
LiveData
etViewModel
simples.
Dans cette série d'ateliers de programmation, vous travaillerez avec l'application NOTES À FAIRE. Elle vous permet de noter des tâches à effectuer et de les afficher sous forme de liste. Vous pouvez ensuite les marquer comme terminées ou non, les filtrer ou les supprimer.
Cette application est écrite en Kotlin, dispose de plusieurs écrans et utilise des composants Jetpack. Elle suit l'architecture d'un guide sur l'architecture des applications. Apprenez à tester cette application pour tester les applications qui utilisent les mêmes bibliothèques et la même architecture.
Pour commencer, téléchargez le code:
Vous pouvez également cloner le dépôt GitHub pour obtenir le code:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout starter_code
Dans cette tâche, vous allez exécuter l'application et explorer le code base.
Étape 1: Exécuter l'exemple d'application
Une fois l'application À faire installée, ouvrez-la dans Android Studio et exécutez-la. Il devrait se compiler. Explorez l'application en procédant comme suit:
- Créez une tâche avec le bouton d'action flottant plus. Saisissez d'abord un titre, puis des informations supplémentaires sur la tâche. Enregistrez-le avec le bouton d'action flottant vert.
- Dans la liste des tâches, cliquez sur leur titre, puis consultez l'écran détaillé de la tâche pour afficher le reste de la description.
- Dans la liste ou sur l'écran des détails, cochez la case correspondant à cette tâche pour définir son état sur Terminé.
- Revenez à l'écran des tâches, ouvrez le menu des filtres et filtrez les tâches par Active et Terminé.
- Ouvrez le panneau de navigation, puis cliquez sur Statistiques.
- Revenez à l'écran de présentation, puis, dans le menu du panneau de navigation, sélectionnez Effacer les tâches terminées pour supprimer toutes les tâches dont l'état est Terminée.
Étape 2: Explorez l'exemple de code de l'application
L'application À FAIRE repose sur l'exemple de test et d'architecture d'architecture Plans d'architecture populaire (qui utilise la version architecture réactive de l'exemple). L'application suit l'architecture d'un guide sur l'architecture des applications. Il utilise ViewModel avec des fragments, un dépôt et une salle. Si vous connaissez l'un des exemples ci-dessous, l'application a une architecture similaire:
- Atelier de programmation Room with a View
- Ateliers de programmation Android Kotlin Fundamentals
- Ateliers de programmation Android avancés
- Exemple de tournesol Android
- Cours de développement d'applications Android avec Kotlin Udacity
Il est plus important de comprendre l'architecture générale de l'application que de maîtriser la logique à n'importe quelle couche.
Voici le résumé des packages que vous trouverez:
Paquet : | |
| Écran d'ajout ou de modification d'une tâche:code du calque d'interface utilisateur permettant d'ajouter ou de modifier une tâche. |
| Couche de données : ce calque gère la couche de données des tâches. Il contient la base de données, le réseau et le code du dépôt. |
| Écran de statistiques:code de l'interface utilisateur correspondant à l'écran des statistiques. |
| Écran des détails de la tâche:code de l'interface utilisateur correspondant à une seule tâche. |
| Écran de tâches:code de l'interface utilisateur pour la liste de toutes les tâches. |
| Cours pratiques:cours partagés utilisés dans différentes sections de l'application (par exemple, pour la mise en page "Balayer l'écran" sur plusieurs écrans). |
Couche de données (.data)
Cette application inclut une couche réseau simulée, dans le package remote et une couche de base de données, dans le package local. Pour plus de simplicité, dans ce projet, la couche réseau est simulée avec un simple HashMap
avec du retard, plutôt que d'effectuer des requêtes réseau réelles.
Les coordonnées DefaultTasksRepository
ou les médiateurs entre la couche réseau et la couche de base de données sont les valeurs qui sont renvoyées à la couche UI.
Couche d'interface utilisateur ( .addedittask, .statistics, .taskdetail, .tasks)
Chacun des packages de la couche de l'interface utilisateur contient un fragment, un modèle de vue et toutes les autres classes requises pour l'interface utilisateur (par exemple, un adaptateur pour la liste de tâches). L'TaskActivity
correspond à l'activité contenant tous les fragments.
Navigation
La navigation dans l'application est contrôlée par le composant Navigation. Elle est définie dans le fichier nav_graph.xml
. La navigation est déclenchée dans les modèles de vue à l'aide de la classe Event
. Les modèles de vue déterminent également les arguments à transmettre. Les fragments observent les Event
et effectuent la navigation réelle entre les écrans.
Dans cette tâche, vous allez exécuter vos premiers tests.
- Dans Android Studio, ouvrez le volet Project (Projet) et recherchez ces trois dossiers:
com.example.android.architecture.blueprints.todoapp
com.example.android.architecture.blueprints.todoapp (androidTest)
com.example.android.architecture.blueprints.todoapp (test)
Ces dossiers sont appelés ensembles sources. Les ensembles sources sont des dossiers contenant le code source de votre application. Les ensembles sources, qui sont en vert (androidTest et test) contiennent vos tests. Par défaut, les trois ensembles suivants sont créés lorsque vous créez un projet Android. à savoir :
main
: contient le code de votre application. Il est partagé entre toutes les différentes versions de l'application que vous pouvez créer (appelées variantes de build).androidTest
: contient des tests instrumentés.test
: contient des tests appelés "tests locaux".
La différence entre les tests locaux et les tests instrumentés réside dans la façon dont ils sont exécutés.
Tests locaux (test
ensemble de sources)
Ces tests sont exécutés en local sur la machine virtuelle de développement et ne nécessitent pas d'émulateur ni d'appareil physique. Ils s'exécutent donc rapidement, mais leur fidélité est inférieure, ce qui signifie qu'ils agissent moins comme ils le feraient dans le monde réel.
Dans Android Studio, les tests en local sont représentés par une icône représentant un triangle vert et rouge.
Tests instrumentés (androidTest
ensemble de sources)
Ces tests s'exécutent sur des appareils Android réels ou émulés. Ils reflètent donc ce qu'il se passera dans le monde réel, mais ils sont également beaucoup plus lents.
Dans Android Studio, les tests instrumentés sont représentés par un Android avec une icône de triangle verte et rouge.
Étape 1: Exécutez un test local
- Ouvrez le dossier
test
jusqu'à ce que vous trouviez le fichier ExampleUnitTest.kt. - Effectuez un clic droit dessus et sélectionnez Run ExampleUnitTest.
Vous devriez obtenir la sortie suivante dans la fenêtre Run (Exécuter) en bas de l'écran:
- Notez que les coches vertes sont développées, puis développez les résultats pour confirmer qu'un test appelé
addition_isCorrect
a réussi. Nous sommes ravis de savoir que l'ajout fonctionne comme prévu.
Étape 2: Échec du test
Voici le test que vous venez d'exécuter.
ExampleUnitTest.kt
// A test class is just a normal class
class ExampleUnitTest {
// Each test is annotated with @Test (this is a Junit annotation)
@Test
fun addition_isCorrect() {
// Here you are checking that 4 is the same as 2+2
assertEquals(4, 2 + 2)
}
}
Notez que les tests
- constituent une classe de l'un des ensembles de sources de test.
- contiennent des fonctions commençant par l'annotation
@Test
(chaque fonction est un test unique). - contiennent généralement des instructions d'assertion.
Android utilise la bibliothèque de test JUnit pour effectuer des tests (dans cet atelier de programmation, JUnit4). Les assertions et l'annotation @Test
proviennent toutes les deux de JUnit.
Une assertion est le cœur de votre test. Il s'agit d'une instruction de code qui vérifie que votre code ou votre application se comporte comme prévu. Dans ce cas, l'assertion est assertEquals(4, 2 + 2)
, qui vérifie que 4 est égal à 2 + 2.
Pour voir à quoi ressemble le test ayant échoué, ajoutez une assertion que vous pouvez facilement voir doit échouer. Cela signifie que 3 est égal à 1+1.
- Ajoutez
assertEquals(3, 1 + 1)
au testaddition_isCorrect
.
ExampleUnitTest.kt
class ExampleUnitTest {
// Each test is annotated with @Test (this is a Junit annotation)
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
assertEquals(3, 1 + 1) // This should fail
}
}
- Exécutez le test.
- Dans les résultats, vérifiez le symbole X à côté du test.
- Autres remarques:
- Une seule assertion infructueuse échoue à l'ensemble du test.
- La valeur attendue (3) correspond à la valeur calculée (2).
- Vous êtes redirigé vers la ligne de l'assertion
(ExampleUnitTest.kt:16)
qui a échoué.
Étape 3: Exécutez un test instrumenté
Les tests instrumentés se trouvent dans l'ensemble de sources androidTest
.
- Ouvrez l'ensemble de sources
androidTest
. - Exécutez le test appelé
ExampleInstrumentedTest
.
Exempleinstrumenté
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.android.architecture.blueprints.reactive",
appContext.packageName)
}
}
Contrairement au test local, ce test s'exécute sur un appareil (dans l'exemple ci-dessous, sur un téléphone Pixel 2 émulé):
Si un appareil est installé ou qu'un émulateur est en cours d'exécution, le test doit s'exécuter sur l'émulateur.
Dans cette tâche, vous allez écrire des tests pour getActiveAndCompleteStats
, qui calcule le pourcentage des statistiques sur les tâches actives et terminées pour votre application. Ces chiffres sont affichés sur l'écran des statistiques de l'application.
Étape 1: Créer un cours test
- Dans l'ensemble de sources
main
, danstodoapp.statistics
, ouvrezStatisticsUtils.kt
. - Recherchez la fonction
getActiveAndCompletedStats
.
StatistiquesUtil.kt
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
val activePercent = 100 * numberOfActiveTasks / totalTasks
val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks
return StatsResult(
activeTasksPercent = activePercent.toFloat(),
completedTasksPercent = completePercent.toFloat()
)
}
data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)
La fonction getActiveAndCompletedStats
accepte une liste de tâches et renvoie un StatsResult
. StatsResult
est une classe de données qui contient deux nombres, le pourcentage de tâches terminées et le pourcentage actif.
Android Studio vous fournit des outils permettant de générer des bouchons de test, afin de mettre en œuvre les tests pour cette fonction.
- Effectuez un clic droit sur
getActiveAndCompletedStats
, puis sélectionnez Generate (Générer) & Test.
La boîte de dialogue Create Test (Créer un test) s'ouvre:
- Remplacez la valeur du champ Nom du cours par
StatisticsUtilsTest
(au lieu deStatisticsUtilsKtTest
, il est légèrement préférable de ne pas inclure le texte KT dans le nom de la classe de test). - Conservez les autres paramètres par défaut. JUnit 4 est la bibliothèque de test appropriée. Le package de destination est correct (il reflète l'emplacement de la classe
StatisticsUtils
). Vous n'avez pas besoin de cocher les cases (cela génère simplement du code supplémentaire, mais vous devez écrire votre test à partir de zéro). - Appuyez sur OK.
La boîte de dialogue Choose Destination Directory (Choisir un répertoire de destination) s'ouvre:
Vous allez effectuer un test local, car votre fonction effectue des calculs mathématiques et n'inclura aucun code spécifique à Android. Il n'est donc pas nécessaire de l'exécuter sur un appareil réel ou émulé.
- Sélectionnez le répertoire
test
(et nonandroidTest
), car vous allez écrire des tests en local. - Cliquez sur OK.
- Notez la classe
StatisticsUtilsTest
générée danstest/statistics/
.
Étape 2: Écrivez votre première fonction de test
Vous allez écrire un test pour vérifier:
- si aucune tâche n'est terminée et si une tâche est active,
- Le pourcentage de tests actifs est de 100 %.
- et le pourcentage de tâches terminées est de 0%.
- Ouvrez
StatisticsUtilsTest
. - Créez une fonction nommée
getActiveAndCompletedStats_noCompleted_returnsHundredZero
.
StatistiquesUtils.kt
class StatisticsUtilsTest {
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task
// Call your function
// Check the result
}
}
- Ajoutez l'annotation
@Test
au-dessus du nom de la fonction pour indiquer qu'il s'agit d'un test. - Créez une liste de tâches.
// Create an active task
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
- Appelez
getActiveAndCompletedStats
pour effectuer ces tâches.
// Call your function
val result = getActiveAndCompletedStats(tasks)
- Vérifiez que
result
correspond à vos attentes à l'aide d'assertions.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
Voici le code complet :
StatistiquesUtils.kt
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task (the false makes this active)
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// Call your function
val result = getActiveAndCompletedStats(tasks)
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
}
}
- Exécutez le test (effectuez un clic droit sur
StatisticsUtilsTest
, puis sélectionnez Run (Exécuter).
Il doit réussir:
Étape 3: Ajoutez la dépendance Hamcrest
Étant donné que vos tests servent à documenter vos opérations, ils sont lisibles lorsqu'ils sont intelligibles. Comparez les deux assertions suivantes:
assertEquals(result.completedTasksPercent, 0f)
// versus
assertThat(result.completedTasksPercent, `is`(0f))
La seconde assertion ressemble davantage à une phrase humaine. Il est écrit à l'aide d'un framework d'assertion appelé Hamcrest. La bibliothèque de vérité est un autre outil efficace pour écrire des assertions lisibles. Vous utiliserez Hamcrest dans cet atelier de programmation pour rédiger des assertions.
- Ouvrez
build.grade (Module: app)
et ajoutez la dépendance suivante.
app/build.gradle
dependencies {
// Other dependencies
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}
En général, vous utilisez implementation
pour ajouter une dépendance, mais c'est ici que vous utilisez testImplementation
. Lorsque vous êtes prêt à partager votre application avec le monde entier, il est préférable de ne pas augmenter la taille de votre APK avec le code ou les dépendances de votre application. Vous pouvez indiquer si une bibliothèque doit être incluse dans le code principal ou de test à l'aide des configurations Gradle. Voici les configurations les plus courantes:
implementation
: la dépendance est disponible dans tous les ensembles de sources, y compris les ensembles de sources de test.testImplementation
: la dépendance n'est disponible que dans l'ensemble de sources sources.androidTestImplementation
: la dépendance n'est disponible que dans l'ensemble de sourcesandroidTest
.
La configuration que vous utilisez définit où les dépendances peuvent être utilisées. Si vous écrivez:
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
Autrement dit, Hamcrest ne sera disponible que dans l'ensemble de sources sources. Elle permet également de s'assurer que Hamcrest ne sera pas inclus dans votre application finale.
Étape 4: Utilisez Hamcrest pour écrire des assertions
- Modifiez le test
getActiveAndCompletedStats_noCompleted_returnsHundredZero()
pour qu'il utiliseassertThat
au lieu deassertEquals
.
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
Notez que vous pouvez utiliser la règle import org.hamcrest.Matchers.`is`
d'importation si vous y êtes invité.
Le test final ressemble au code ci-dessous.
StatistiquesUtils.kt
import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
// Create an active tasks (the false makes this active)
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// Call your function
val result = getActiveAndCompletedStats(tasks)
// Check the result
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
}
}
- Exécutez le nouveau test pour vérifier qu'il fonctionne toujours.
Cet atelier de programmation n'apprend pas tous les aspects de Hamcrest. Par conséquent, si vous souhaitez en savoir plus, consultez le tutoriel officiel.
Cette opération est facultative.
Dans cette tâche, vous allez écrire d'autres tests à l'aide de JUnit et de Hamcrest. Vous allez également écrire des tests à l'aide d'une stratégie dérivée de la pratique du programme Test Driven Development. Le développement piloté par le test est un programme de pensée qui suppose que, au lieu d'écrire votre code de fonctionnalité, vous devez d'abord écrire vos tests. Vous écrivez ensuite le code de votre caractéristique, dans le but de réussir vos tests.
Étape 1 : Écrire les tests
Rédigez des tests pour les cas où une liste de tâches normale est disponible:
- Si une tâche est terminée, mais qu'aucune tâche n'est active, le pourcentage
activeTasks
doit être0f
, et le pourcentage des tâches terminées doit être100f
. - S'il y a deux tâches terminées et trois tâches actives, le pourcentage terminé doit être
40f
, et le pourcentage actif doit être60f
.
Étape 2. Rédiger un test pour un bug
Le code de la getActiveAndCompletedStats
, tel qu'il est écrit, comporte un bug. Notez qu'elle ne gère pas correctement ce qui se passe si la liste est vide ou nulle. Dans ces deux cas, les deux pourcentages doivent être nuls.
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
val activePercent = 100 * numberOfActiveTasks / totalTasks
val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks
return StatsResult(
activeTasksPercent = activePercent.toFloat(),
completedTasksPercent = completePercent.toFloat()
)
}
Pour corriger le code et écrire des tests, vous allez utiliser le développement basé sur les tests. Suivez les étapes ci-dessous pour le développement piloté par le test.
- Rédigez votre test à l'aide de la structure "Donnée, quand, puis," et avec un nom qui respecte la convention.
- Vérifiez que le test échoue.
- Rédigez le code minimal pour réussir le test.
- Répétez l'opération pour tous les tests.
Au lieu de commencer par corriger le bug, vous commencerez par écrire les tests. Vous pourrez ensuite vérifier que des tests vous protègent contre la réapparition accidentelle de ces bugs à l'avenir.
- Si la liste est vide (
emptyList()
), les deux pourcentages doivent être nuls. - Si une erreur s'est produite lors du chargement des tâches, la liste affiche
null
et les deux pourcentages doivent être nuls. - Exécutez vos tests et vérifiez qu'ils échouent :
Étape 3. Corrigez le bug
Maintenant que vous avez terminé vos tests, corrigez le bug.
- Corrigez le bug
getActiveAndCompletedStats
en affichant0f
sitasks
estnull
ou vide:
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
- Exécutez à nouveau vos tests et vérifiez que tous les tests sont désormais réussis.
Après avoir suivi le TDD et rédigé les tests en premier, vous avez ainsi pu:
- La nouvelle fonctionnalité est toujours associée à des tests. Par conséquent, vos tests servent de documentation sur les actions de votre code.
- Vos tests permettent de vérifier l'exactitude des résultats et de vous protéger contre les bugs que vous avez déjà constatés.
Solution: écrire davantage de tests
Voici tous les tests et le code de fonctionnalité correspondant.
StatistiquesUtils.kt
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
val tasks = listOf(
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed with an active task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 100 and 0
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
val tasks = listOf(
Task("title", "desc", isCompleted = true)
)
// When the list of tasks is computed with a completed task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 0 and 100
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(100f))
}
@Test
fun getActiveAndCompletedStats_both_returnsFortySixty() {
// Given 3 completed tasks and 2 active tasks
val tasks = listOf(
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = false),
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed
val result = getActiveAndCompletedStats(tasks)
// Then the result is 40-60
assertThat(result.activeTasksPercent, `is`(40f))
assertThat(result.completedTasksPercent, `is`(60f))
}
@Test
fun getActiveAndCompletedStats_error_returnsZeros() {
// When there's an error loading stats
val result = getActiveAndCompletedStats(null)
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_empty_returnsZeros() {
// When there are no tasks
val result = getActiveAndCompletedStats(emptyList())
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
}
StatistiquesUtil.kt
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
Vous maîtrisez bien les bases pour écrire et exécuter des tests ! Vous allez maintenant apprendre à écrire des tests ViewModel
et LiveData
de base.
Dans la suite de l'atelier de programmation, vous apprendrez à écrire des tests pour deux classes Android courantes pour la plupart des applications (ViewModel
et LiveData
).
Vous allez commencer par écrire les tests pour le TasksViewModel
.
Vous allez vous concentrer sur les tests dont la logique est déterminée dans le modèle d'affichage et qui n'ont pas recours au code du dépôt. Le code du dépôt implique du code asynchrone, des bases de données et des appels réseau, qui ajoutent une complexité de test. Vous allez éviter cela pour l'instant et vous concentrer sur l'écriture de tests pour la fonctionnalité ViewModel qui ne teste directement rien dans le dépôt.
Le test que vous écrirez vérifiera que, lorsque vous appelez la méthode addNewTask
, le Event
permettant d'ouvrir la nouvelle fenêtre de tâche est déclenché. Voici le code d'application que vous allez tester.
TasksViewModel.kt
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
Étape 1 : Créer une classe TasksViewModelTest
À l'aide de la même procédure que pour StatisticsUtilTest
, vous créez un fichier de test pour TasksViewModelTest
.
- Ouvrez la classe que vous souhaitez tester, dans le package
tasks
,TasksViewModel.
. - Dans le code, effectuez un clic droit sur le nom de la classe
TasksViewModel
->, Generate ->, Générer.
- Sur l'écran Create Test (Créer un test), cliquez sur OK pour accepter (vous n'avez pas besoin de modifier les paramètres par défaut).
- Dans la boîte de dialogue Choose Destination Directory (Choisir un répertoire de destination), sélectionnez le répertoire test.
Étape 2. Commencer à écrire votre test ViewModel
Au cours de cette étape, vous allez ajouter un test de modèle de vue pour vérifier que, lorsque vous appelez la méthode addNewTask
, le paramètre Event
permettant d'ouvrir la nouvelle fenêtre de tâche est déclenché.
- Créez un test appelé
addNewTask_setsNewTaskEvent
.
TâchesViewModelTest.kt
class TasksViewModelTest {
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh TasksViewModel
// When adding a new task
// Then the new task event is triggered
}
}
Qu'en est-il du contexte de l'application ?
Lorsque vous créez une instance de TasksViewModel
à tester, son constructeur nécessite un contexte d'application. Mais dans ce test, vous n'allez pas créer d'application complète avec des activités, des interfaces utilisateur et des fragments. Alors, comment obtenir un contexte d'application ?
TâchesViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(???)
Les bibliothèques de test AndroidX incluent des classes et des méthodes qui vous fournissent des versions des composants tels que les applications et les activités destinées aux tests. Lorsque vous avez besoin d'un test local dans lequel vous avez besoin de simuler des classes du framework Android (telles qu'un contexte d'application), procédez comme suit pour configurer correctement AndroidX Test:
- Ajouter les dépendances de base et d'ext. AndroidX Test
- Ajouter la dépendance Robolectric Testing
- Annoter la classe avec le lanceur de test AndroidJunit4
- Rédiger le code AndroidX Test
Vous allez suivre cette procédure, puis puis comprendre ce qu'ils font ensemble.
Étape 3. Ajouter les dépendances de Gradle
- Copiez ces dépendances dans le fichier
build.gradle
du module de votre application pour ajouter les dépendances principales de l'outil de test AndroidX et de l'ext., ainsi que la dépendance des tests Robolectric.
app/build.gradle
// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
Étape 4 : Ajouter un lanceur de test JUnit
- Ajoutez
@RunWith(AndroidJUnit4::class)
au-dessus de votre classe de test.
TâchesViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}
Étape 5 : Utiliser AndroidX Test
À ce stade, vous pouvez utiliser la bibliothèque test AndroidX. Cela inclut la méthode ApplicationProvider.getApplicationContex
t
, qui obtient un contexte d'application.
- Créez un
TasksViewModel
à l'aide deApplicationProvider.getApplicationContext()
à partir de la bibliothèque de test AndroidX.
TâchesViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
- Appeler
addNewTask
autasksViewModel
.
TâchesViewModelTest.kt
tasksViewModel.addNewTask()
À ce stade, votre test doit ressembler à ce qui suit.
TâchesViewModelTest.kt
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
// TODO test LiveData
}
- Exécutez votre test pour vérifier qu'il fonctionne.
Concept: comment fonctionne AndroidX Test ?
Qu'est-ce qu'AndroidX Test ?
AndroidX Test est un ensemble de bibliothèques destinées aux tests. Il inclut des classes et des méthodes qui fournissent des versions des composants tels que Applications et Activités, destinées aux tests. Par exemple, ce code que vous avez écrit est un exemple de fonction de test AndroidX permettant d'obtenir un contexte d'application.
ApplicationProvider.getApplicationContext()
L'un des avantages des API AndroidX Test est qu'elles sont conçues pour fonctionner à la fois pour les tests locaux et les tests instrumentés. C'est intéressant:
- Vous pouvez effectuer le même test qu'un test local ou instrumenté.
- Vous n'avez pas besoin d'apprendre à utiliser différentes API de test pour les tests locaux et instrumentés.
Par exemple, comme vous avez écrit votre code à l'aide de bibliothèques de test AndroidX, vous pouvez déplacer votre classe TasksViewModelTest
du dossier test
vers le dossier androidTest
. Les tests continueront donc d'être exécutés. Le fonctionnement de getApplicationContext()
est légèrement différent selon qu'il s'agit d'un test local ou instrumenté:
- S'il s'agit d'un test instrumenté, le contexte d'application réel sera fourni lors du démarrage de l'émulateur ou de la connexion à un appareil réel.
- S'il s'agit d'un test local, elle utilise un environnement Android simulé.
Quel est le rôle de Robolectric ?
L'environnement Android simulé utilisé par AndroidX Test pour les tests locaux est fourni par Robolectric. Robolectric est une bibliothèque qui crée un environnement Android simulé pour effectuer des tests et s'exécute plus rapidement que le démarrage d'un émulateur ou l'exécution sur un appareil. Sans la dépendance Robolectric, vous obtiendrez ce message d'erreur:
À quoi sert @RunWith(AndroidJUnit4::class)
?
Un exécuteur de test est un composant JUnit qui exécute des tests. Sans testeur, vos tests ne seraient pas exécutés. Il s'agit d'un lanceur de test par défaut fourni par JUnit que vous recevez automatiquement. @RunWith
remplace cet appareil de test par défaut.
Le lanceur de test AndroidJUnit4
permet à AndroidX Test d'exécuter votre test différemment selon qu'il s'agit de tests locaux ou instrumentés.
Étape 6 : Corriger les avertissements vétérinaires
Lorsque vous exécutez le code, vous remarquerez que Robolectric est utilisé.
Grâce à AndroidX Test et au testeur AndroidJunit4, vous n'avez pas besoin d'écrire directement une ligne de code Robolectric.
Vous remarquerez peut-être deux avertissements.
No such manifest file: ./AndroidManifest.xml
"WARN: Android SDK 29 requires Java 9..."
Vous pouvez résoudre l'avertissement No such manifest file: ./AndroidManifest.xml
en mettant à jour votre fichier Gradle.
- Ajoutez la ligne suivante à votre fichier Gradle pour utiliser le fichier manifeste Android approprié. L'option includeAndroidResources vous permet d'accéder aux ressources Android de vos tests unitaires, y compris votre fichier AndroidManifest.
app/build.gradle
// Always show the result of every unit test when running via command line, even if it passes.
testOptions.unitTests {
includeAndroidResources = true
// ...
}
L'avertissement "WARN: Android SDK 29 requires Java 9..."
est plus compliqué. Exécuter des tests sur Android Q nécessite Java 9. Au lieu d'essayer de configurer Android Studio pour utiliser Java 9, pour cet atelier de programmation, conservez la cible 28 et votre SDK de compilation.
En résumé :
- Les tests de modèle avec vue pure peuvent généralement être intégrés à l'ensemble de sources
test
, car leur code ne nécessite pas Android. - Vous pouvez utiliser la bibliothèque de test AndroidX pour obtenir les versions de test des composants tels que "Applications" et "Activités".
- Si vous devez exécuter du code Android simulé dans votre ensemble source
test
, vous pouvez ajouter la dépendance Robolectric et l'annotation@RunWith(AndroidJUnit4::class)
.
Félicitations, vous utilisez à la fois la bibliothèque de test AndroidX et Robolectric pour effectuer un test. Votre test n'est pas terminé (vous n'avez pas encore rédigé de déclaration, vous devez simplement dire // TODO test LiveData
). Vous apprendrez à écrire des déclarations de confidentialité avec LiveData
.
Dans cette tâche, vous allez apprendre à revendiquer correctement la valeur LiveData
.
Voici où vous vous êtes arrêté sans addNewTask_setsNewTaskEvent
le test de modèle de vue.
TâchesViewModelTest.kt
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
// TODO test LiveData
}
Pour tester LiveData
, nous vous recommandons d'effectuer deux opérations:
- Utiliser
InstantTaskExecutorRule
- Assurer l'observation de
LiveData
Étape 1 : Utiliser InstantTaskExecutorRule
InstantTaskExecutorRule
est une règle JUnit. Lorsque vous l'utilisez avec l'annotation @get:Rule
, elle entraîne l'exécution de code dans la classe InstantTaskExecutorRule
avant et après les tests (pour afficher le code exact, vous pouvez utiliser le raccourci clavier Commande+B pour afficher le fichier).
Cette règle exécute toutes les tâches d'arrière-plan associées aux composants d'architecture dans le même thread, de sorte que les résultats du test se produisent de manière synchrone et dans un ordre reproductible. Lorsque vous écrivez des tests qui incluent le test des données LiveData, utilisez cette règle !
- Ajoutez la dépendance Gradle à la bibliothèque de tests de base des composants d'architecture (qui contient cette règle).
app/build.gradle
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
- Ouvrir
TasksViewModelTest.kt
- Ajoutez le
InstantTaskExecutorRule
à l'intérieur de la classeTasksViewModelTest
.
TâchesViewModelTest.kt
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Other code...
}
Étape 2. Ajouter la classe LiveDataTestUtil.kt
L'étape suivante consiste à s'assurer que les tests LiveData
sont respectés.
Lorsque vous utilisez LiveData
, vous avez généralement une activité ou un fragment (LifecycleOwner
) observez le LiveData
.
viewModel.resultLiveData.observe(fragment, Observer {
// Observer code here
})
Cette observation est importante. Vous avez besoin d'observateurs actifs sur LiveData
pour
- déclencher un événement
onChanged
. - déclencher des transformations.
Pour obtenir le comportement LiveData
attendu de votre LiveData
(modèle de vue), vous devez observer LiveData
avec LifecycleOwner
.
Cela pose un problème dans votre test TasksViewModel
, vous n'avez pas d'activité ni de fragment pour observer votre LiveData
. Pour contourner ce problème, vous pouvez utiliser la méthode observeForever
, qui assure que le LiveData
est constamment observé, sans avoir besoin de LifecycleOwner
. Lorsque vous observeForever
, vous devez penser à supprimer votre observateur ou à risque de fuite.
Voici un exemple de code : Examinez-la:
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// Create observer - no need for it to do anything!
val observer = Observer<Event<Unit>> {}
try {
// Observe the LiveData forever
tasksViewModel.newTaskEvent.observeForever(observer)
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.value
assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
} finally {
// Whatever happens, don't forget to remove the observer!
tasksViewModel.newTaskEvent.removeObserver(observer)
}
}
Cela représente beaucoup de code récurrent pour observer un seul élément LiveData
dans un test ! Il existe plusieurs façons de vous débarrasser de vos plaques chauffantes. Vous allez créer une fonction d'extension appelée LiveDataTestUtil
pour simplifier l'ajout d'observateurs.
- Créez un fichier Kotlin nommé
LiveDataTestUtil.kt
dans votre ensemble sourcetest
.
- Copiez et collez le code ci-dessous.
LiveDataTestUtil.kt.
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
Cette méthode est assez compliquée. Il crée une fonction d'extension Kotlin appelée getOrAwaitValue
, qui ajoute un observateur, obtient la valeur LiveData
, puis nettoie l'observateur. Globalement, cette version du code observeForever
est réutilisable. Pour obtenir une explication complète de ce cours, consultez cet article de blog.
Étape 3. Utiliser getOrAwaitValue pour écrire l'assertion
Au cours de cette étape, vous allez utiliser la méthode getOrAwaitValue
et rédiger une instruction affirmant que le newTaskEvent
a été déclenché.
- Obtenez la valeur
LiveData
pournewTaskEvent
à l'aide degetOrAwaitValue
.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
- Déclarez que la valeur n'est pas nulle.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))
Le test complet doit ressembler à ce qui suit.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled(), not(nullValue()))
}
}
- Exécutez votre code et regardez le test réussi !
Maintenant que vous savez comment écrire un test, créez-en un seul. Au cours de cette étape, vous allez utiliser les compétences que vous avez apprises et vous entraîner à rédiger un autre test TasksViewModel
.
Étape 1 : Écrire votre propre test ViewModel
Vous allez écrire setFilterAllTasks_tasksAddViewVisible()
. Ce test doit vérifier que si vous avez défini un type de filtre pour afficher toutes les tâches, le bouton Ajouter une tâche est visible.
- En utilisant
addNewTask_setsNewTaskEvent()
pour référence, écrivez un test dansTasksViewModelTest
appelésetFilterAllTasks_tasksAddViewVisible()
qui définit le mode de filtrage surALL_TASKS
et affirme quetasksAddViewVisible
LiveData esttrue
.
Pour commencer, utilisez le code ci-dessous.
TasksViewModelTest
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
// When the filter type is ALL_TASKS
// Then the "Add task" action is visible
}
Remarque :
- L'énumération
TasksFilterType
pour toutes les tâches estALL_TASKS.
- La visibilité du bouton permettant d'ajouter une tâche est contrôlée par le paramètre
tasksAddViewVisible.
deLiveData
- Exécutez votre test.
Étape 2. Comparez votre test à la solution
Comparez votre solution à celle ci-dessous.
TasksViewModelTest
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
}
Vérifiez si vous effectuez les actions suivantes:
- Vous créez votre
tasksViewModel
à l'aide de la même instructionApplicationProvider.getApplicationContext()
AndroidX. - Vous appelez la méthode
setFiltering
en transmettant l'énumération du type de filtreALL_TASKS
. - Vous pouvez vérifier que la valeur de
tasksAddViewVisible
est vraie avec la méthodegetOrAwaitNextValue
.
Étape 3. Ajouter une règle @Before
Notez qu'au début de vos deux tests, vous devez définir une TasksViewModel
.
TasksViewModelTest
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
Lorsque le code de configuration est répété pour plusieurs tests, vous pouvez utiliser l'annotation @Before pour créer une méthode de configuration et supprimer le code répété. Étant donné que tous ces tests vont tester le TasksViewModel
et qu'ils ont besoin d'un modèle de vue, déplacez ce code vers un bloc @Before
.
- Créez une variable d'instance
lateinit
appeléetasksViewModel|
. - Créez une méthode appelée
setupViewModel
. - Annotez-le avec
@Before
. - Déplacez le code d'instanciation de modèle de vue vers
setupViewModel
.
TasksViewModelTest
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
- Exécutez votre code !
Avertissement
N'effectuez pas les actions suivantes. Ne procédez pas à l'initialisation.
tasksViewModel
avec sa définition:
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
La même instance sera alors utilisée pour tous les tests. Nous vous recommandons d'éviter cette situation, car chaque test doit comporter une nouvelle instance du sujet testé (ViewModel, dans ce cas).
Le code final pour TasksViewModelTest
devrait ressembler à celui ci-dessous.
TasksViewModelTest
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
@Test
fun addNewTask_setsNewTaskEvent() {
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.awaitNextValue()
assertThat(
value?.getContentIfNotHandled(), (not(nullValue()))
)
}
@Test
fun getTasksAddViewVisible() {
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
}
}
Cliquez ici pour afficher la différence entre le code initial et le code final.
Pour télécharger le code de l'atelier de programmation terminé, vous pouvez utiliser la commande Git ci-dessous:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.
Cet atelier de programmation aborde les sujets suivants:
- Exécuter des tests depuis Android Studio
- Différence entre les tests locaux (
test
) et d'instrumentation (androidTest
). - Écrire des tests unitaires locaux à l'aide de JUnit et de Hamcrest
- Configurer des tests ViewModel avec la bibliothèque de test AndroidX
Cours Udacity:
Documentation pour les développeurs Android:
- Guide de l'architecture des applications
- JUnit 4
- Hamcrest
- Bibliothèque de tests Robolectric
- Bibliothèque de tests AndroidX
- Bibliothèque principale des composants d'architecture AndroidX
- ensembles de sources
- Effectuer un test à partir de la ligne de commande
Vidéos :
Autre :
Pour obtenir des liens vers d'autres ateliers de programmation dans ce cours, consultez la page de destination "Avancé Android" dans les ateliers de programmation Kotlin.