Principes de base des tests

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:

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:

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 et ViewModel 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:

Télécharger le fichier ZIP

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:

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 : com.example.android.architecture.blueprints.todoapp

.addedittask

É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.

.data

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.

.statistics

Écran de statistiques:code de l'interface utilisateur correspondant à l'écran des statistiques.

.taskdetail

Écran des détails de la tâche:code de l'interface utilisateur correspondant à une seule tâche.

.tasks

Écran de tâches:code de l'interface utilisateur pour la liste de toutes les tâches.

.util

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.

  1. 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

  1. Ouvrez le dossier test jusqu'à ce que vous trouviez le fichier ExampleUnitTest.kt.
  2. 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:

  1. 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.

  1. Ajoutez assertEquals(3, 1 + 1) au test addition_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
   }
}
  1. Exécutez le test.
  1. Dans les résultats, vérifiez le symbole X à côté du test.

  1. 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.

  1. Ouvrez l'ensemble de sources androidTest.
  2. 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

  1. Dans l'ensemble de sources main, dans todoapp.statistics, ouvrez StatisticsUtils.kt.
  2. 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.

  1. 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:

  1. Remplacez la valeur du champ Nom du cours par StatisticsUtilsTest (au lieu de StatisticsUtilsKtTest, il est légèrement préférable de ne pas inclure le texte KT dans le nom de la classe de test).
  2. 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).
  3. 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é.

  1. Sélectionnez le répertoire test (et non androidTest), car vous allez écrire des tests en local.
  2. Cliquez sur OK.
  3. Notez la classe StatisticsUtilsTest générée dans test/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%.
  1. Ouvrez StatisticsUtilsTest.
  2. 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
    }
}
  1. Ajoutez l'annotation @Test au-dessus du nom de la fonction pour indiquer qu'il s'agit d'un test.
  2. Créez une liste de tâches.
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. Appelez getActiveAndCompletedStats pour effectuer ces tâches.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. 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)
    }
}
  1. 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.

  1. 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 sources androidTest.

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

  1. Modifiez le test getActiveAndCompletedStats_noCompleted_returnsHundredZero() pour qu'il utilise assertThat au lieu de assertEquals.
// 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))

    }
}
  1. 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:

  1. Si une tâche est terminée, mais qu'aucune tâche n'est active, le pourcentage activeTasks doit être 0f, et le pourcentage des tâches terminées doit être 100f .
  2. 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 être 60f.

É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.

  1. Rédigez votre test à l'aide de la structure "Donnée, quand, puis," et avec un nom qui respecte la convention.
  2. Vérifiez que le test échoue.
  3. Rédigez le code minimal pour réussir le test.
  4. 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.

  1. Si la liste est vide (emptyList()), les deux pourcentages doivent être nuls.
  2. Si une erreur s'est produite lors du chargement des tâches, la liste affiche null et les deux pourcentages doivent être nuls.
  3. 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.

  1. Corrigez le bug getActiveAndCompletedStats en affichant 0f si tasks est null 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
        )
    }
}
  1. 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.

  1. Ouvrez la classe que vous souhaitez tester, dans le package tasks, TasksViewModel..
  2. Dans le code, effectuez un clic droit sur le nom de la classe TasksViewModel -&gt, Generate -&gt, Générer.

  1. 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).
  2. 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é.

  1. 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:

  1. Ajouter les dépendances de base et d'ext. AndroidX Test
  2. Ajouter la dépendance Robolectric Testing
  3. Annoter la classe avec le lanceur de test AndroidJunit4
  4. 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

  1. 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

  1. 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.getApplicationContext, qui obtient un contexte d'application.

  1. Créez un TasksViewModel à l'aide de ApplicationProvider.getApplicationContext() à partir de la bibliothèque de test AndroidX.

TâchesViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. Appeler addNewTask au tasksViewModel.

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
    }
  1. 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.

  1. 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:

  1. Utiliser InstantTaskExecutorRule
  2. 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 !

  1. 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"
  1. Ouvrir TasksViewModelTest.kt
  2. Ajoutez le InstantTaskExecutorRule à l'intérieur de la classe TasksViewModelTest.

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

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.

  1. Créez un fichier Kotlin nommé LiveDataTestUtil.kt dans votre ensemble source test.


  1. 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é.

  1. Obtenez la valeur LiveData pour newTaskEvent à l'aide de getOrAwaitValue.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. 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()))


    }

}
  1. 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.

  1. En utilisant addNewTask_setsNewTaskEvent() pour référence, écrivez un test dans TasksViewModelTest appelé setFilterAllTasks_tasksAddViewVisible() qui définit le mode de filtrage sur ALL_TASKS et affirme que tasksAddViewVisibleLiveData est true.


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 est ALL_TASKS.
  • La visibilité du bouton permettant d'ajouter une tâche est contrôlée par le paramètre tasksAddViewVisible. de LiveData
  1. 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 instruction ApplicationProvider.getApplicationContext() AndroidX.
  • Vous appelez la méthode setFiltering en transmettant l'énumération du type de filtre ALL_TASKS.
  • Vous pouvez vérifier que la valeur de tasksAddViewVisible est vraie avec la méthode getOrAwaitNextValue.

É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.

  1. Créez une variable d'instance lateinit appelée tasksViewModel|.
  2. Créez une méthode appelée setupViewModel.
  3. Annotez-le avec @Before.
  4. 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())
    }
  1. 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.

Télécharger le fichier ZIP

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:

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.