Principes de base des tests

Cet atelier de programmation fait partie du cours Développement Android avancé en Kotlin. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre, mais ce n'est pas obligatoire. Tous les ateliers de programmation du cours sont listés sur la page de destination des ateliers de programmation Android avancé en 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, bien qu'il s'agisse d'un test manuel. À mesure que vous ajoutiez et mettiez à jour des fonctionnalités, vous avez probablement continué à exécuter votre code et à vérifier qu'il fonctionnait. Mais le faire manuellement à chaque fois est fatigant, source d'erreurs et non évolutif.

Les ordinateurs sont excellents pour le scaling et l'automatisation. C'est pourquoi les développeurs des entreprises de toutes tailles écrivent des tests automatisés, qui sont des tests exécutés par un logiciel et qui ne nécessitent pas que vous utilisiez manuellement l'application pour vérifier que le code fonctionne.

Dans cette série d'ateliers de programmation, vous allez apprendre à créer une collection de tests (appelée suite de tests) pour une application réelle.

Ce premier atelier de programmation couvre les principes de base des tests sur Android. Vous allez écrire vos premiers tests et apprendre à tester les LiveData et les ViewModel.

Ce que vous devez déjà savoir

Vous devez maîtriser les éléments suivants :

Points abordés

Vous allez aborder les sujets suivants :

  • Écrire et exécuter des tests unitaires sur Android
  • Utiliser le développement piloté par les tests
  • Choisir entre les tests instrumentés et les tests locaux

Vous allez découvrir les bibliothèques et les concepts de code suivants :

Objectifs de l'atelier

  • Configurer, exécuter et interpréter les tests locaux et instrumentés dans Android
  • Écrivez des tests unitaires dans Android à l'aide de JUnit4 et Hamcrest.
  • Écrivez des tests LiveData et ViewModel simples.

Dans cette série d'ateliers de programmation, vous allez utiliser l'application TO-DO Notes. Elle vous permet de noter les tâches à accomplir et de les afficher dans une liste. Vous pouvez ensuite les marquer comme terminées ou non, les filtrer ou les supprimer.

Cette application est écrite en Kotlin, comporte plusieurs écrans, utilise des composants Jetpack et suit l'architecture d'un Guide de l'architecture des applications. En apprenant à tester cette application, vous pourrez 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 la base de code.

Étape 1 : Exécutez l'application exemple

Une fois l'application TO-DO téléchargé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 à l'aide du bouton d'action flottant Plus. Saisissez d'abord un titre, puis ajoutez des informations sur la tâche. Enregistrez-le à l'aide du bouton d'action flottant vert.
  • Dans la liste des tâches, cliquez sur le titre de la tâche que vous venez de terminer et consultez l'écran d'informations pour voir le reste de la description.
  • Dans la liste ou sur l'écran d'informations, cochez la case de la tâche pour définir son état sur Terminée.
  • Revenez à l'écran des tâches, ouvrez le menu de filtre et filtrez les tâches par état Active et Terminée.
  • Ouvrez le panneau de navigation, puis cliquez sur Statistiques.
  • Revenez à l'écran "Vue d'ensemble", 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 : Explorer le code de l'application exemple

L'application TO-DO est basée sur l'exemple d'architecture et de test Architecture Blueprints (qui utilise la version architecture réactive de l'exemple). L'application suit l'architecture du Guide de l'architecture des applications. Il utilise des ViewModels avec des Fragments, un dépôt et Room. Si vous connaissez l'un des exemples ci-dessous, sachez que cette application possède une architecture similaire :

Il est plus important que vous compreniez l'architecture générale de l'application que d'avoir une compréhension approfondie de la logique à un niveau donné.

Voici un récapitulatif des packages disponibles :

Package  : com.example.android.architecture.blueprints.todoapp

.addedittask

Écran "Ajouter ou modifier une tâche" : code de la couche UI pour ajouter ou modifier une tâche.

.data

Couche de données : elle concerne la couche de données des tâches. Il contient le code de la base de données, du réseau et du dépôt.

.statistics

Écran des statistiques : code de la couche d'UI pour l'écran des statistiques.

.taskdetail

Écran des détails d'une tâche : code de la couche UI pour une tâche unique.

.tasks

Écran des tâches : code de la couche UI pour la liste de toutes les tâches.

.util

Classes utilitaires : classes partagées utilisées dans différentes parties de l'application, par exemple pour la mise en page d'actualisation par balayage utilisée 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 simplifier, dans ce projet, la couche réseau est simulée avec un simple HashMap avec un délai, plutôt que d'effectuer de véritables requêtes réseau.

DefaultTasksRepository coordonne ou sert d'intermédiaire entre la couche réseau et la couche de base de données. C'est ce qui renvoie les données à la couche d'UI.

Couche d'UI ( .addedittask, .statistics, .taskdetail, .tasks)

Chacun des packages de la couche UI contient un fragment et un modèle de vue, ainsi que toutes les autres classes requises pour l'UI (comme un adaptateur pour la liste des tâches). TaskActivity est l'activité qui contient tous les fragments.

Navigation

La navigation dans l'application est contrôlée par le composant Navigation. Il est défini 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 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 les trois dossiers suivants :
  • 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 de sources. Les ensembles de sources sont des dossiers contenant le code source de votre application. Les ensembles de sources, qui sont de couleur verte (androidTest et test), contiennent vos tests. Lorsque vous créez un projet Android, vous obtenez par défaut les trois ensembles de sources suivants. Les voici :

  • main : contient le code de votre application. Ce code est partagé entre toutes les versions de l'application que vous pouvez compiler (appelées variantes de compilation).
  • androidTest : contient des tests appelés tests instrumentés.
  • test : contient les 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 (ensemble de sources test)

Ces tests sont exécutés localement sur la JVM de votre machine de développement et ne nécessitent pas d'émulateur ni d'appareil physique. Elles s'exécutent rapidement, mais leur fidélité est plus faible, ce qui signifie qu'elles agissent moins comme elles le feraient dans le monde réel.

Dans Android Studio, les tests locaux sont représentés par une icône en forme de triangle vert et rouge.

Tests instrumentés (ensemble de sources androidTest)

Ces tests s'exécutent sur des appareils Android réels ou émulés. Ils reflètent donc ce qui se passera dans le monde réel, mais sont également beaucoup plus lents.

Dans Android Studio, les tests instrumentés sont représentés par un Android avec une icône en forme de triangle vert 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, puis sélectionnez Run ExampleUnitTest (Exécuter ExampleUnitTest).

Le résultat suivant devrait s'afficher dans la fenêtre Run (Exécuter) en bas de l'écran :

  1. Notez les coches vertes et développez les résultats des tests pour confirmer qu'un test appelé addition_isCorrect a réussi. C'est bien de savoir que l'ajout fonctionne comme prévu.

Étape 2 : Faire échouer le test

Vous trouverez ci-dessous 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

  • sont une classe dans l'un des ensembles de sources de test.
  • contiennent des fonctions qui commencent 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 les tests (JUnit4 dans cet atelier de programmation). Les assertions et l'annotation @Test proviennent de JUnit.

Une assertion est au cœur de votre test. Il s'agit d'une instruction de code qui vérifie que votre code ou votre application se sont comportés comme prévu. Dans ce cas, l'assertion est assertEquals(4, 2 + 2), qui vérifie que 4 = 2 + 2.

Pour voir à quoi ressemble un test qui a échoué, ajoutez une assertion qui, selon vous, devrait échouer. Il vérifiera que 3 = 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 du test, vous remarquez un X à côté du test.

  1. Remarque :
  • Si une seule assertion échoue, l'ensemble du test échoue.
  • Vous êtes informé de la valeur attendue (3) par rapport à la valeur réellement calculée (2).
  • Vous êtes redirigé vers la ligne de l'assertion ayant échoué (ExampleUnitTest.kt:16).

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

ExampleInstrumentedTest

@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, un téléphone Pixel 2 émulé) :

Si un appareil est connecté ou qu'un émulateur est en cours d'exécution, vous devriez voir le test s'exécuter sur l'émulateur.

Dans cette tâche, vous allez écrire des tests pour getActiveAndCompleteStats, qui calcule le pourcentage de statistiques de tâches actives et terminées pour votre application. Vous pouvez voir ces chiffres sur l'écran des statistiques de l'application.

Étape 1 : Créer une classe de test

  1. Dans l'ensemble de sources main, dans todoapp.statistics, ouvrez StatisticsUtils.kt.
  2. Recherchez la fonction getActiveAndCompletedStats.

StatisticsUtils.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 de tâches actives.

Android Studio vous fournit des outils pour générer des bouchons de test qui vous aident à implémenter les tests pour cette fonction.

  1. Effectuez un clic droit sur getActiveAndCompletedStats, puis sélectionnez Generate > Test (Générer > Test).

La boîte de dialogue Créer un test s'ouvre :

  1. Remplacez Nom de la classe par StatisticsUtilsTest (au lieu de StatisticsUtilsKtTest, car il est préférable de ne pas avoir KT dans le nom de la classe de test).
  2. Conservez les autres valeurs 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 de cases (cela génère simplement du code supplémentaire, mais vous allez écrire votre test à partir de zéro).
  3. Appuyez sur OK.

La boîte de dialogue Choisir le répertoire de destination s'ouvre :

Vous allez effectuer un test local, car votre fonction effectue des calculs mathématiques et n'inclut 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 locaux.
  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 qui vérifie les points suivants :

  • s'il n'y a aucune tâche terminée et une tâche active ;
  • que 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.

StatisticsUtilsTest.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 avec ces tâches.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. Vérifiez que result correspond à ce que vous attendiez, à l'aide d'assertions.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

Voici le code complet :

StatisticsUtilsTest.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 (clic droit StatisticsUtilsTest, puis sélectionnez Exécuter).

Il devrait réussir :

Étape 3 : Ajoutez la dépendance Hamcrest

Comme vos tests servent de documentation sur ce que fait votre code, il est préférable qu'ils soient lisibles par un humain. Comparez les deux assertions suivantes :

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

La deuxième affirmation ressemble beaucoup plus à une phrase humaine. Il est écrit à l'aide d'un framework d'assertion appelé Hamcrest. La bibliothèque Truth est un autre outil utile pour écrire des assertions lisibles. Dans cet atelier de programmation, vous utiliserez Hamcrest pour écrire 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 lorsque vous ajoutez une dépendance, mais ici, 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 du code de test ou des dépendances dans 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 de test.
  • androidTestImplementation : la dépendance n'est disponible que dans l'ensemble de sources androidTest.

La configuration que vous utilisez définit l'emplacement où la dépendance peut être utilisée. Si vous écrivez :

testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

Cela signifie que Hamcrest ne sera disponible que dans l'ensemble de sources de test. Cela garantit également que Hamcrest ne sera pas inclus dans votre application finale.

Étape 4 : Utiliser Hamcrest pour écrire des assertions

  1. Mettez à jour le test getActiveAndCompletedStats_noCompleted_returnsHundredZero() pour utiliser assertThat de Hamcrest 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 l'importation import org.hamcrest.Matchers.`is` si vous y êtes invité.

Le test final ressemblera au code ci-dessous.

StatisticsUtilsTest.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 votre test mis à jour pour vérifier qu'il fonctionne toujours.

Cet atelier de programmation ne vous apprendra pas tout ce qu'il y a à savoir sur Hamcrest. Si vous souhaitez en savoir plus, consultez le tutoriel officiel.

Il s'agit d'une tâche facultative pour vous entraîner.

Dans cette tâche, vous allez écrire d'autres tests à l'aide de JUnit et Hamcrest. Vous écrirez également des tests à l'aide d'une stratégie dérivée de la pratique de programmation Test Driven Development. Le développement piloté par les tests (TDD, Test-Driven Development) est une approche de programmation qui consiste à écrire les tests avant le code de la fonctionnalité. Vous écrivez ensuite le code de votre fonctionnalité dans le but de réussir vos tests.

Étape 1 : Écrire les tests

Écrivez des tests pour les cas où vous avez une liste de tâches normale :

  1. S'il y a une tâche terminée et aucune tâche active, le pourcentage activeTasks doit être de 0f et le pourcentage de tâches terminées doit être de 100f .
  2. Si deux tâches sont terminées et trois sont actives, le pourcentage de tâches terminées doit être de 40f et celui des tâches actives de 60f.

Étape 2 : Écrire un test pour un bug

Le code du getActiveAndCompletedStats tel qu'il est écrit contient un bug. Notez qu'il 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 utiliserez le développement piloté par les tests. Le développement piloté par les tests suit les étapes suivantes.

  1. Écrivez le test en utilisant la structure "Étant donné, Quand, Alors" et en lui donnant un nom qui suit la convention.
  2. Vérifiez que le test échoue.
  3. Écrivez le code minimal pour que le test réussisse.
  4. Répétez l'opération pour tous les tests.

Au lieu de commencer par corriger le bug, vous allez d'abord écrire les tests. Vous pouvez ensuite confirmer que vous disposez de tests qui 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 définis sur 0f.
  2. Si une erreur s'est produite lors du chargement des tâches, la liste sera null et les deux pourcentages seront de 0.
  3. Exécutez vos tests et vérifiez qu'ils échouent :

Étape 3 : Corriger le bug

Maintenant que vous avez vos tests, corrigez le bug.

  1. Corrigez le bug dans getActiveAndCompletedStats en renvoyant 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 qu'ils réussissent tous.

En suivant le développement piloté par les tests et en écrivant les tests en premier, vous avez contribué à garantir les points suivants :

  • Chaque nouvelle fonctionnalité est associée à des tests. Vos tests servent donc de documentation sur ce que fait votre code.
  • Vos tests vérifient que les résultats sont corrects et vous protègent contre les bugs que vous avez déjà rencontrés.

Solution : Écrire plus de tests

Voici tous les tests et le code de fonctionnalité correspondant.

StatisticsUtilsTest.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))
    }
}

StatisticsUtils.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
        )
    }
}

Bravo pour avoir appris les bases de l'écriture et de l'exécution de tests ! Vous allez maintenant apprendre à écrire des tests ViewModel et LiveData de base.

Dans le reste de l'atelier de programmation, vous apprendrez à écrire des tests pour deux classes Android courantes dans la plupart des applications : ViewModel et LiveData.

Vous commencez par écrire des tests pour TasksViewModel.


Vous allez vous concentrer sur les tests dont toute la logique se trouve dans le modèle de vue et qui ne dépendent pas du 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 tous de la complexité aux tests. Pour l'instant, vous allez éviter cela 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 allez écrire vérifiera que l'Event pour ouvrir la nouvelle fenêtre de tâche est déclenché lorsque vous appelez la méthode addNewTask. Voici le code de l'application que vous allez tester.

TasksViewModel.kt

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

Étape 1 : Créer une classe TasksViewModelTest

En suivant les mêmes étapes que pour StatisticsUtilTest, vous allez créer 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 > Generate > Test.

  1. Sur l'écran 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 (Sélectionner un répertoire de destination), sélectionnez le répertoire test.

Étape 2 : Commencer à écrire votre test ViewModel

Dans cette étape, vous allez ajouter un test de modèle de vue pour vérifier que lorsque vous appelez la méthode addNewTask, le Event pour ouvrir la fenêtre de nouvelle tâche est déclenché.

  1. Créez un test nommé addNewTask_setsNewTaskEvent.

TasksViewModelTest.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 pour effectuer des tests, son constructeur nécessite un contexte d'application. Toutefois, dans ce test, vous ne créez pas une application complète avec des activités, une interface utilisateur et des fragments. Comment obtenir un contexte d'application ?

TasksViewModelTest.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 de composants tels que les applications et les activités, conçues pour les tests. Si vous avez un test local pour lequel vous avez besoin de classes de framework Android simulées(comme un contexte d'application), suivez ces étapes pour configurer correctement AndroidX Test :

  1. Ajouter les dépendances principales et d'extension AndroidX Test
  2. Ajouter la dépendance de la bibliothèque de test Robolectric
  3. Annoter la classe avec le testeur AndroidJunit4
  4. Écrire du code AndroidX Test

Vous allez suivre ces étapes, puis comprendre ce qu'elles font ensemble.

Étape 3 : Ajouter les dépendances Gradle

  1. Copiez ces dépendances dans le fichier build.gradle du module de votre application pour ajouter les dépendances AndroidX Test core et ext, ainsi que la dépendance de test 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 JUnit Test Runner

  1. Ajoutez @RunWith(AndroidJUnit4::class)au-dessus de votre classe de test.

TasksViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

Étape 5 : Utiliser AndroidX Test

À ce stade, vous pouvez utiliser la bibliothèque de 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() de la bibliothèque de test AndroidX.

TasksViewModelTest.kt

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

TasksViewModelTest.kt

tasksViewModel.addNewTask()

À ce stade, votre test devrait ressembler au code ci-dessous.

TasksViewModelTest.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 de test. Il inclut des classes et des méthodes qui vous fournissent des versions de composants tels que les applications et les activités, qui sont destinées aux tests. Par exemple, le code que vous avez écrit est un exemple de fonction AndroidX Test 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 d'instrumentation. Voici pourquoi c'est intéressant :

  • Vous pouvez exécuter le même test en tant que test local ou test d'instrumentation.
  • Vous n'avez pas besoin d'apprendre différentes API de test pour les tests locaux et instrumentés.

Par exemple, comme vous avez écrit votre code à l'aide des bibliothèques AndroidX Test, vous pouvez déplacer votre classe TasksViewModelTest du dossier test vers le dossier androidTest et les tests s'exécuteront toujours. Le getApplicationContext() fonctionne légèrement différemment selon qu'il est exécuté en tant que test local ou instrumenté :

  • S'il s'agit d'un test instrumenté, il obtient le contexte d'application réel fourni lors du démarrage d'un émulateur ou de la connexion à un appareil réel.
  • S'il s'agit d'un test local, il utilise un environnement Android simulé.

Qu'est-ce que Robolectric ?

L'environnement Android simulé qu'AndroidX Test utilise pour les tests locaux est fourni par Robolectric. Robolectric est une bibliothèque qui crée un environnement Android simulé pour les 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 cette erreur :

Que fait @RunWith(AndroidJUnit4::class) ?

Un exécuteur de test est un composant JUnit qui exécute des tests. Sans un exécuteur de tests, vos tests ne s'exécuteraient pas. JUnit fournit un exécuteur de test par défaut que vous obtenez automatiquement. @RunWith remplace cet exécuteur de tests par défaut.

L'exécuteur de test AndroidJUnit4 permet à AndroidX Test d'exécuter vos tests différemment selon qu'il s'agit de tests instrumentés ou locaux.

Étape 6 : Résoudre les avertissements Robolectric

Lorsque vous exécutez le code, notez que Robolectric est utilisé.

Grâce à AndroidX Test et au test runner AndroidJUnit4, cela se fait sans que vous ayez à écrire directement une seule 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 corriger l'avertissement No such manifest file: ./AndroidManifest.xml en mettant à jour votre fichier Gradle.

  1. Ajoutez la ligne suivante à votre fichier Gradle pour que le fichier manifeste Android approprié soit utilisé. L'option includeAndroidResources vous permet d'accéder aux ressources Android dans 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 complexe. L'exécution de tests sur Android Q nécessite Java 9. Au lieu d'essayer de configurer Android Studio pour qu'il utilise Java 9, conservez le SDK cible et de compilation à 28 pour cet atelier de programmation.

En résumé :

  • Les tests de ViewModel pur peuvent généralement être placés dans l'ensemble de sources test, car leur code ne nécessite généralement pas Android.
  • Vous pouvez utiliser la bibliothèque de test AndroidX pour obtenir des versions de test de composants tels que les applications et les activités.
  • Si vous devez exécuter du code Android simulé dans votre ensemble de sources 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 exécuter un test. Votre test n'est pas terminé (vous n'avez pas encore écrit d'instruction assert, il indique simplement // TODO test LiveData). Vous apprendrez à écrire des instructions assert avec LiveData ensuite.

Dans cette tâche, vous allez apprendre à affirmer correctement la valeur LiveData.

Reprenons là où vous en étiez sans le test du modèle de vue addNewTask_setsNewTaskEvent.

TasksViewModelTest.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 de faire deux choses :

  1. Utiliser InstantTaskExecutorRule
  2. Assurer l'observation LiveData

Étape 1 : Utiliser InstantTaskExecutorRule

InstantTaskExecutorRule est une règle JUnit. Lorsque vous l'utilisez avec l'annotation @get:Rule, il exécute du code dans la classe InstantTaskExecutorRule avant et après les tests (pour voir le code exact, vous pouvez utiliser le raccourci clavier Cmd+B pour afficher le fichier).

Cette règle exécute tous les jobs en arrière-plan liés aux composants d'architecture dans le même thread afin que les résultats des tests se produisent de manière synchrone et dans un ordre reproductible. Utilisez cette règle lorsque vous écrivez des tests qui incluent le test de LiveData.

  1. Ajoutez la dépendance Gradle pour la bibliothèque de test principale 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 InstantTaskExecutorRule dans la classe TasksViewModelTest.

TasksViewModelTest.kt

class TasksViewModelTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    // Other code...
}

Étape 2 : Ajouter la classe LiveDataTestUtil.kt

L'étape suivante consiste à vous assurer que le LiveData que vous testez est observé.

Lorsque vous utilisez LiveData, une activité ou un fragment (LifecycleOwner) observe généralement le LiveData.

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
})

Cette observation est importante. Vous devez avoir des observateurs actifs sur LiveData pour

Pour obtenir le comportement LiveData attendu pour le LiveData de votre modèle de vue, vous devez observer le LiveData avec un 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 garantit que LiveData est constamment observé, sans avoir besoin d'un LifecycleOwner. Lorsque vous observeForever, n'oubliez pas de supprimer votre observateur pour éviter une fuite d'observateur.

Le code devrait ressembler à l'exemple ci-dessous. Examinez-le :

@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 fait beaucoup de code récurrent pour observer un seul LiveData dans un test ! Il existe plusieurs façons de se débarrasser de ce code passe-partout. Vous allez créer une fonction d'extension appelée LiveDataTestUtil pour simplifier l'ajout d'observateurs.

  1. Créez un fichier Kotlin appelé LiveDataTestUtil.kt dans votre ensemble de sources 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
}

Il s'agit d'une méthode assez complexe. Il crée une fonction d'extension Kotlin appelée getOrAwaitValue, qui ajoute un observateur, obtient la valeur LiveData, puis nettoie l'observateur. Il s'agit en fait d'une version courte et réutilisable du code observeForever présenté ci-dessus. Pour une explication complète de cette classe, consultez cet article de blog.

Étape 3 : Utiliser getOrAwaitValue pour écrire l'assertion

Dans cette étape, vous allez utiliser la méthode getOrAwaitValue et écrire une instruction assert qui vérifie que newTaskEvent a été déclenché.

  1. Récupérez la valeur LiveData pour newTaskEvent à l'aide de getOrAwaitValue.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. Affirmez que la valeur n'est pas nulle.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

Le test complet devrait se présenter comme 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 vérifiez que le test est réussi.

Maintenant que vous avez vu comment écrire un test, écrivez-en un vous-même. Dans cette étape, mettez en pratique les compétences que vous avez acquises et écrivez un autre test TasksViewModel.

Étape 1 : Écrire votre propre test ViewModel

Vous écrirez setFilterAllTasks_tasksAddViewVisible(). Ce test doit vérifier que le bouton Ajouter une tâche est visible si vous avez défini votre type de filtre sur "Afficher toutes les tâches".

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


 Utilisez le code ci-dessous pour commencer.

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

        // Then the "Add task" action is visible
        
    }

Remarques :

  • 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 LiveData tasksAddViewVisible..
  1. Exécutez votre test.

Étape 2 : Comparer 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 opérations suivantes :

  • Vous créez votre tasksViewModel en utilisant 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 vérifiez que tasksAddViewVisible est défini sur "true" à l'aide de la méthode getOrAwaitNextValue.

Étape 3 : Ajouter une règle @Before

Remarquez comment vous définissez un TasksViewModel au début de vos deux tests.

TasksViewModelTest

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Lorsque vous avez un code de configuration 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 TasksViewModel et ont besoin d'un ViewModel, 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 du 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 opérations suivantes : n'initialisez pas

tasksViewModel

avec sa définition :

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Cela entraînera l'utilisation de la même instance pour tous les tests. Vous devez éviter cela, car chaque test doit disposer d'une nouvelle instance du sujet testé (ViewModel dans ce cas).

Votre code final pour TasksViewModelTest devrait ressembler au code 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 voir une comparaison entre le code de départ 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

Voici ce que vous avez appris dans cet atelier de programmation :

  • Exécuter des tests depuis Android Studio
  • Différence entre les tests en local (test) et les tests d'instrumentation (androidTest).
  • Écrire des tests unitaires locaux à l'aide de JUnit et Hamcrest.
  • Configurer des tests ViewModel avec la bibliothèque de test AndroidX.

Cours Udacity :

Documentation pour les développeurs Android :

Vidéos :

Autre :

Pour accéder aux autres ateliers de programmation de ce cours, consultez la page de destination des ateliers de programmation "Développement Android avancé en Kotlin".