Présentation des doubles de test et de l'injection de dépendances

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

Ce deuxième atelier de programmation sur les tests porte sur les doubles de test : quand les utiliser sur Android et comment les implémenter à l'aide de l'injection de dépendances, du modèle Service Locator et des bibliothèques. Vous apprendrez ainsi à rédiger :

  • Tests unitaires du dépôt
  • Tests d'intégration des fragments et des ViewModel
  • Tests de navigation par fragment

Ce que vous devez déjà savoir

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

Points abordés

  • Planifier une stratégie de test
  • Créer et utiliser des doubles de test, à savoir des faux et des mocks
  • Utiliser l'injection manuelle de dépendances sur Android pour les tests unitaires et d'intégration
  • Appliquer le modèle Service Locator
  • Tester les dépôts, les fragments, les ViewModels et le composant Navigation

Vous utiliserez les bibliothèques et les concepts de code suivants :

Objectifs de l'atelier

  • Écrivez des tests unitaires pour un dépôt à l'aide d'un test double et de l'injection de dépendances.
  • Écrivez des tests unitaires pour un modèle de vue à l'aide d'un test double et de l'injection de dépendances.
  • Écrivez des tests d'intégration pour les fragments et leurs ViewModels à l'aide du framework de test d'UI Espresso.
  • Écrivez des tests de navigation à l'aide de Mockito et Espresso.

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

Télécharger le code

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 end_codelab_1

Prenez quelques instants pour vous familiariser avec le code en suivant les instructions ci-dessous.

É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 cet atelier de programmation, vous apprendrez à tester des dépôts, des vues de modèle et des fragments à l'aide de doubles de test et de l'injection de dépendances. Avant de vous pencher sur ces tests, il est important de comprendre le raisonnement qui guidera ce que vous allez écrire et comment.

Cette section aborde certaines bonnes pratiques de test en général, telles qu'elles s'appliquent à Android.

La pyramide de tests

Lorsque vous réfléchissez à une stratégie de test, trois aspects sont à prendre en compte :

  • Portée : quelle partie du code le test couvre-t-il ? Les tests peuvent s'exécuter sur une seule méthode, sur l'ensemble de l'application ou sur une partie de celle-ci.
  • Vitesse : à quelle vitesse le test s'exécute-t-il ? La durée des tests peut varier de quelques millisecondes à plusieurs minutes.
  • Fidélité : dans quelle mesure le test est-il "réaliste" ? Par exemple, si une partie du code que vous testez doit effectuer une requête réseau, le code de test effectue-t-il réellement cette requête réseau ou simule-t-il le résultat ? Si le test communique réellement avec le réseau, cela signifie qu'il est plus fidèle. L'inconvénient est que le test peut prendre plus de temps à s'exécuter, entraîner des erreurs si le réseau est hors service ou être coûteux à utiliser.

Il existe des compromis inhérents entre ces aspects. Par exemple, la vitesse et la fidélité sont un compromis : plus le test est rapide, moins il est fidèle, et inversement. Il existe trois catégories courantes de tests automatisés :

  • Tests unitaires : il s'agit de tests très ciblés qui s'exécutent sur une seule classe, généralement une seule méthode de cette classe. Si un test unitaire échoue, vous pouvez identifier précisément l'emplacement du problème dans votre code. Ils ont une faible fidélité, car dans le monde réel, votre application implique bien plus que l'exécution d'une méthode ou d'une classe. Ils sont suffisamment rapides pour être exécutés chaque fois que vous modifiez votre code. Il s'agit le plus souvent de tests exécutés en local (dans l'ensemble de sources test). Exemple  : tester des méthodes uniques dans les ViewModels et les dépôts.
  • Tests d'intégration : ils testent l'interaction de plusieurs classes pour s'assurer qu'elles se comportent comme prévu lorsqu'elles sont utilisées ensemble. Une façon de structurer les tests d'intégration consiste à les faire tester une seule fonctionnalité, comme la possibilité d'enregistrer une tâche. Ils testent une plus grande partie du code que les tests unitaires, mais sont toujours optimisés pour s'exécuter rapidement, plutôt que d'avoir une fidélité totale. Ils peuvent être exécutés localement ou en tant que tests d'instrumentation, selon la situation. Exemple  : Tester toutes les fonctionnalités d'une paire fragment/modèle de vue unique.
  • Tests de bout en bout : testez une combinaison de fonctionnalités qui fonctionnent ensemble. Ils testent de grandes parties de l'application, simulent une utilisation réelle de manière précise et sont donc généralement lents. Elles offrent la plus grande fidélité et vous indiquent si votre application fonctionne réellement dans son ensemble. En général, ces tests seront des tests instrumentés (dans l'ensemble de sources androidTest)
    Exemple  : démarrer l'application entière et tester quelques fonctionnalités ensemble.

La proportion suggérée de ces tests est souvent représentée par une pyramide, la grande majorité des tests étant des tests unitaires.

Architecture et tests

Votre capacité à tester votre application à tous les niveaux de la pyramide de tests est intrinsèquement liée à l'architecture de votre application. Par exemple, une application extrêmement mal conçue peut placer toute sa logique dans une seule méthode. Vous pouvez écrire un test de bout en bout pour cela, car ces tests ont tendance à tester de grandes parties de l'application. Mais qu'en est-il des tests unitaires ou d'intégration ? Avec tout le code au même endroit, il est difficile de tester uniquement le code lié à une seule unité ou fonctionnalité.

Une meilleure approche consisterait à décomposer la logique de l'application en plusieurs méthodes et classes, ce qui permettrait de tester chaque élément de manière isolée. L'architecture est un moyen de diviser et d'organiser votre code, ce qui facilite les tests unitaires et d'intégration. L'application de tâches à tester suit une architecture particulière :



Dans ce cours, vous allez découvrir comment tester des parties de l'architecture ci-dessus, de manière isolée :

  1. Vous allez d'abord tester l'unité du dépôt.
  2. Vous utiliserez ensuite un test double dans le modèle de vue, ce qui est nécessaire pour les tests unitaires et les tests d'intégration du modèle de vue.
  3. Vous allez ensuite apprendre à écrire des tests d'intégration pour les fragments et leurs modèles de vue.
  4. Enfin, vous apprendrez à écrire des tests d'intégration qui incluent le composant Navigation.

Les tests de bout en bout seront abordés dans la prochaine leçon.

Lorsque vous écrivez un test unitaire pour une partie d'une classe (une méthode ou un petit ensemble de méthodes), votre objectif est de tester uniquement le code de cette classe.

Il peut être difficile de tester uniquement le code d'une ou de plusieurs classes spécifiques. Prenons un exemple. Ouvrez la classe data.source.DefaultTaskRepository dans l'ensemble de sources main. Il s'agit du dépôt de l'application et de la classe pour laquelle vous allez écrire des tests unitaires.

Votre objectif est de tester uniquement le code de cette classe. Toutefois, DefaultTaskRepository dépend d'autres classes, telles que LocalTaskDataSource et RemoteTaskDataSource, pour fonctionner. Autrement dit, LocalTaskDataSource et RemoteTaskDataSource sont des dépendances de DefaultTaskRepository.

Ainsi, chaque méthode de DefaultTaskRepository appelle des méthodes sur les classes de source de données, qui à leur tour appellent des méthodes dans d'autres classes pour enregistrer des informations dans une base de données ou communiquer avec le réseau.



Par exemple, examinez cette méthode dans DefaultTasksRepo.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks est l'un des appels les plus "basiques" que vous puissiez effectuer à votre dépôt. Cette méthode inclut la lecture à partir d'une base de données SQLite et l'exécution d'appels réseau (l'appel à updateTasksFromRemoteDataSource). Cela implique beaucoup plus de code que juste le code du dépôt.

Voici quelques raisons plus spécifiques pour lesquelles il est difficile de tester le dépôt :

  • Vous devez réfléchir à la création et à la gestion d'une base de données pour effectuer les tests les plus simples pour ce dépôt. Cela soulève des questions telles que "Doit-il s'agir d'un test local ou instrumenté ?" et si vous devez utiliser AndroidX Test pour obtenir un environnement Android simulé.
  • Certaines parties du code, comme le code réseau, peuvent prendre beaucoup de temps à s'exécuter, voire échouer de temps en temps, ce qui crée des tests de longue durée et instables.
  • Vos tests pourraient perdre leur capacité à diagnostiquer le code à l'origine d'un échec de test. Vos tests peuvent commencer à tester du code non lié au dépôt. Par exemple, vos tests unitaires "de dépôt" supposés peuvent échouer en raison d'un problème dans une partie du code dépendant, comme le code de la base de données.

Doubles de test

La solution consiste à ne pas utiliser le véritable code réseau ou de base de données lorsque vous testez le dépôt, mais plutôt un test double. Un test double est une version d'une classe conçue spécifiquement pour les tests. Il est destiné à remplacer la version réelle d'une classe dans les tests. C'est un peu comme une doublure cascadeur, qui est un acteur spécialisé dans les cascades et qui remplace l'acteur principal pour les actions dangereuses.

Voici quelques types de doubles de test :

Faux

Un test double qui dispose d'une implémentation"fonctionnelle" de la classe, mais qui est implémenté de manière à être adapté aux tests, mais pas à la production.

Simulation

Un test double qui suit les méthodes qui ont été appelées. Il réussit ou échoue ensuite un test selon que ses méthodes ont été appelées correctement.

Stub

Un test double qui n'inclut aucune logique et ne renvoie que ce que vous lui demandez de renvoyer. Un StubTaskRepository peut être programmé pour renvoyer certaines combinaisons de tâches à partir de getTasks, par exemple.

Faux

Un test double qui est transmis, mais pas utilisé, par exemple si vous avez juste besoin de le fournir en tant que paramètre. Si vous aviez un NoOpTaskRepository, il implémenterait simplement le TaskRepository sans code dans aucune des méthodes.

Espion

Un double de test qui suit également certaines informations supplémentaires. Par exemple, si vous avez créé un SpyTaskRepository, il peut suivre le nombre de fois où la méthode addTask a été appelée.

Pour en savoir plus sur les doubles de test, consultez Testing on the Toilet: Know Your Test Doubles.

Les doubles de test les plus courants utilisés dans Android sont les fakes et les mocks.

Dans cette tâche, vous allez créer un double de test FakeDataSource pour tester l'unité DefaultTasksRepository indépendamment des sources de données réelles.

Étape 1 : Créer la classe FakeDataSource

À cette étape, vous allez créer une classe appelée FakeDataSouce, qui sera un test double de LocalDataSource et RemoteDataSource.

  1. Dans l'ensemble de sources test, effectuez un clic droit et sélectionnez New -> Package (Nouveau -> Package).

  1. Créez un package de données avec un package source à l'intérieur.
  2. Créez une classe nommée FakeDataSource dans le package data/source.

Étape 2 : Implémenter l'interface TasksDataSource

Pour pouvoir utiliser votre nouvelle classe FakeDataSource comme test double, elle doit pouvoir remplacer les autres sources de données. Ces sources de données sont TasksLocalDataSource et TasksRemoteDataSource.

  1. Notez que les deux implémentent l'interface TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Faites en sorte que FakeDataSource implémente TasksDataSource :
class FakeDataSource : TasksDataSource {

}

Android Studio vous indiquera que vous n'avez pas implémenté les méthodes requises pour TasksDataSource.

  1. Utilisez le menu de correction rapide et sélectionnez Implémenter les membres.


  1. Sélectionnez toutes les méthodes, puis appuyez sur OK.

Étape 3 : Implémentez la méthode getTasks dans FakeDataSource

FakeDataSource est un type spécifique de test double appelé fake. Un faux est un double de test qui dispose d'une implémentation"fonctionnelle" de la classe, mais qui est implémenté de manière à être adapté aux tests, mais pas à la production. Une implémentation "fonctionnelle" signifie que la classe produira des sorties réalistes à partir des entrées.

Par exemple, votre fausse source de données ne se connectera pas au réseau ni n'enregistrera quoi que ce soit dans une base de données. Elle utilisera simplement une liste en mémoire. Cela "fonctionnera comme prévu", car les méthodes permettant d'obtenir ou d'enregistrer des tâches renverront les résultats attendus. Toutefois, vous ne pourrez jamais utiliser cette implémentation en production, car elle n'est pas enregistrée sur le serveur ni dans une base de données.

FakeDataSource

  • vous permet de tester le code dans DefaultTasksRepository sans avoir besoin de s'appuyer sur une véritable base de données ou un véritable réseau.
  • fournit une implémentation "suffisamment réelle" pour les tests.
  1. Modifiez le constructeur FakeDataSource pour créer un var appelé tasks qui est un MutableList<Task>? avec une valeur par défaut de liste mutable vide.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Il s'agit de la liste des tâches qui "simulent" une réponse de base de données ou de serveur. Pour l'instant, l'objectif est de tester la méthode getTasks du dépôt . Cela appelle les méthodes getTasks, deleteAllTasks et saveTask de la source de données .

Écrivez une version factice de ces méthodes :

  1. Écrivez getTasks : si tasks n'est pas null, renvoyez un résultat Success. Si tasks est null, renvoie un résultat Error.
  2. Écrivez deleteAllTasks : effacez la liste des tâches modifiables.
  3. Écrivez saveTask : ajoutez la tâche à la liste.

Ces méthodes, implémentées pour FakeDataSource, ressemblent au code ci-dessous.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

Voici les instructions d'importation, si nécessaire :

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

Cela ressemble au fonctionnement des sources de données locales et distantes réelles.

Dans cette étape, vous allez utiliser une technique appelée injection de dépendances manuelle pour pouvoir utiliser le faux test double que vous venez de créer.

Le problème principal est que vous disposez d'un FakeDataSource, mais que son utilisation dans les tests n'est pas claire. Il doit remplacer TasksRemoteDataSource et TasksLocalDataSource, mais uniquement dans les tests. TasksRemoteDataSource et TasksLocalDataSource sont des dépendances de DefaultTasksRepository, ce qui signifie que DefaultTasksRepositories nécessite ou "dépend" de ces classes pour s'exécuter.

Pour le moment, les dépendances sont construites dans la méthode init de DefaultTasksRepository.

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

Étant donné que vous créez et attribuez taskLocalDataSource et tasksRemoteDataSource dans DefaultTasksRepository, ils sont essentiellement codés en dur. Il n'existe aucun moyen d'insérer votre double de test.

Au lieu de les coder en dur, vous devez fournir ces sources de données à la classe. La fourniture de dépendances est appelée injection de dépendances. Il existe différentes manières de fournir des dépendances et, par conséquent, différents types d'injection de dépendances.

L'injection de dépendances du constructeur vous permet d'insérer le test double en le transmettant au constructeur.

Aucune injection

Injection

Étape 1 : Utilisez l'injection de dépendances du constructeur dans DefaultTasksRepository

  1. Modifiez le constructeur de DefaultTaskRepository pour qu'il accepte les sources de données et le répartiteur de coroutines (que vous devrez également remplacer pour vos tests : cela est décrit plus en détail dans la section sur les coroutines de la troisième leçon).Application

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Comme vous avez transmis les dépendances, supprimez la méthode init. Vous n'avez plus besoin de créer les dépendances.
  2. Supprimez également les anciennes variables d'instance. Vous les définissez dans le constructeur :

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Enfin, mettez à jour la méthode getRepository pour utiliser le nouveau constructeur :

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

Vous utilisez maintenant l'injection de dépendances de constructeur.

Étape 2 : Utilisez votre FakeDataSource dans vos tests

Maintenant que votre code utilise l'injection de dépendances du constructeur, vous pouvez utiliser votre source de données factices pour tester votre DefaultTasksRepository.

  1. Effectuez un clic droit sur le nom de la classe DefaultTasksRepository, puis sélectionnez Generate (Générer), puis Test (Tester).
  2. Suivez les instructions pour créer DefaultTasksRepositoryTest dans l'ensemble de sources test.
  3. En haut de votre nouvelle classe DefaultTasksRepositoryTest, ajoutez les variables de membre ci-dessous pour représenter les données de vos fausses sources de données.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Créez trois variables, deux variables membres FakeDataSource (une pour chaque source de données de votre dépôt) et une variable pour le DefaultTasksRepository que vous allez tester.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Créez une méthode pour configurer et initialiser un DefaultTasksRepository testable. Ce DefaultTasksRepository utilisera votre double de test, FakeDataSource.

  1. Créez une méthode appelée createRepository et annotez-la avec @Before.
  2. Instanciez vos sources de données fictives à l'aide des listes remoteTasks et localTasks.
  3. Instanciez votre tasksRepository, en utilisant les deux fausses sources de données que vous venez de créer et Dispatchers.Unconfined.

La méthode finale doit ressembler au code ci-dessous.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

Étape 3 : Écrire le test getTasks() de DefaultTasksRepository

Il est temps d'écrire un test DefaultTasksRepository !

  1. Écrivez un test pour la méthode getTasks du dépôt. Vérifiez que lorsque vous appelez getTasks avec true (ce qui signifie qu'il doit recharger à partir de la source de données distante), il renvoie les données de la source de données distante (par opposition à la source de données locale).

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

Une erreur s'affichera lorsque vous appellerez getTasks:.

Étape 4 : Ajouter runBlockingTest

L'erreur de coroutine est attendue, car getTasks est une fonction suspend et vous devez lancer une coroutine pour l'appeler. Pour cela, vous avez besoin d'un champ d'application de coroutine. Pour résoudre cette erreur, vous devez ajouter des dépendances Gradle pour gérer le lancement des coroutines dans vos tests.

  1. Ajoutez les dépendances requises pour tester les coroutines à l'ensemble de sources de test à l'aide de testImplementation.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

N'oubliez pas de synchroniser !

kotlinx-coroutines-test est la bibliothèque de test des coroutines, spécialement conçue pour tester les coroutines. Pour exécuter vos tests, utilisez la fonction runBlockingTest. Il s'agit d'une fonction fournie par la bibliothèque de tests de coroutines. Il prend un bloc de code, puis l'exécute dans un contexte de coroutine spécial qui s'exécute de manière synchrone et immédiate, ce qui signifie que les actions se produiront dans un ordre déterministe. Cela permet essentiellement à vos coroutines de s'exécuter comme des non-coroutines. Cette méthode est donc destinée au code de test.

Utilisez runBlockingTest dans vos classes de test lorsque vous appelez une fonction suspend. Vous en apprendrez davantage sur le fonctionnement de runBlockingTest et sur la façon de tester les coroutines dans le prochain atelier de programmation de cette série.

  1. Ajoutez @ExperimentalCoroutinesApi au-dessus de la classe. Cela indique que vous savez que vous utilisez une API de coroutine expérimentale (runBlockingTest) dans la classe. Sinon, vous recevrez un avertissement.
  2. De retour dans votre DefaultTasksRepositoryTest, ajoutez runBlockingTest pour que l'ensemble de votre test soit considéré comme un "bloc" de code.

Ce test final ressemble au code ci-dessous.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. Exécutez votre nouveau test getTasks_requestsAllTasksFromRemoteDataSource et vérifiez qu'il fonctionne et que l'erreur a disparu.

Vous venez de voir comment tester un dépôt. Dans les étapes suivantes, vous allez à nouveau utiliser l'injection de dépendances et créer un autre test double. Cette fois, vous allez montrer comment écrire des tests unitaires et d'intégration pour vos modèles de vue.

Les tests unitaires ne doivent tester que la classe ou la méthode qui vous intéresse. Il s'agit de tests en isolement, où vous isolez clairement votre "unité" et ne testez que le code qui en fait partie.

Par conséquent, TasksViewModelTest ne doit tester que le code TasksViewModel. Il ne doit pas tester les classes de base de données, de réseau ni de dépôt. Par conséquent, pour vos ViewModel, comme vous venez de le faire pour votre dépôt, vous allez créer un faux dépôt et appliquer l'injection de dépendances pour l'utiliser dans vos tests.

Dans cette tâche, vous allez appliquer l'injection de dépendances aux modèles de vue.

Étape 1 : Créer une interface TasksRepository

La première étape pour utiliser l'injection de dépendances de constructeur consiste à créer une interface commune partagée entre la classe fictive et la classe réelle.

Comment cela se traduit-il concrètement ? Examinez TasksRemoteDataSource, TasksLocalDataSource et FakeDataSource. Vous remarquerez qu'ils partagent tous la même interface : TasksDataSource. Cela vous permet de spécifier dans le constructeur de DefaultTasksRepository que vous acceptez un TasksDataSource.

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

C'est ce qui nous permet de remplacer votre FakeDataSource.

Ensuite, créez une interface pour DefaultTasksRepository, comme vous l'avez fait pour les sources de données. Il doit inclure toutes les méthodes publiques (surface de l'API publique) de DefaultTasksRepository.

  1. Ouvrez DefaultTasksRepository, puis effectuez un clic droit sur le nom du cours. Sélectionnez ensuite Refactor > Extract > Interface (Refactoriser > Extraire > Interface).

  1. Sélectionnez Extraire dans un fichier distinct.

  1. Dans la fenêtre Extract Interface (Extraire l'interface), remplacez le nom de l'interface par TasksRepository.
  2. Dans la section Membres à former, cochez tous les membres sauf les deux membres compagnons et les méthodes privées.


  1. Cliquez sur Refactor (Refactoriser). La nouvelle interface TasksRepository devrait apparaître dans le package data/source .

DefaultTasksRepository implémente désormais TasksRepository.

  1. Exécutez votre application (et non les tests) pour vous assurer que tout fonctionne toujours correctement.

Étape 2 : Créer FakeTestRepository

Maintenant que vous disposez de l'interface, vous pouvez créer le double de test DefaultTaskRepository.

  1. Dans l'ensemble de sources test, dans data/source, créez le fichier et la classe Kotlin FakeTestRepository.kt et étendez-les à partir de l'interface TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Vous serez invité à implémenter les méthodes d'interface.

  1. Pointez sur l'erreur jusqu'à ce que le menu de suggestions s'affiche, puis cliquez et sélectionnez Implémenter les membres.
  1. Sélectionnez toutes les méthodes, puis appuyez sur OK.

Étape 3 : Implémenter les méthodes FakeTestRepository

Vous disposez maintenant d'une classe FakeTestRepository avec des méthodes "non implémentées". Comme pour l'implémentation de FakeDataSource, FakeTestRepository sera soutenu par une structure de données, au lieu de gérer une médiation complexe entre les sources de données locales et distantes.

Notez que votre FakeTestRepository n'a pas besoin d'utiliser des FakeDataSource ni rien de ce genre. Il doit simplement renvoyer de fausses sorties réalistes en fonction des entrées. Vous utiliserez un LinkedHashMap pour stocker la liste des tâches et un MutableLiveData pour vos tâches observables.

  1. Dans FakeTestRepository, ajoutez une variable LinkedHashMap représentant la liste actuelle des tâches et un MutableLiveData pour vos tâches observables.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

Implémentez les méthodes suivantes :

  1. getTasks : cette méthode doit prendre le tasksServiceData et le transformer en liste à l'aide de tasksServiceData.values.toList(), puis renvoyer le résultat sous la forme d'un Success.
  2. refreshTasks : met à jour la valeur de observableTasks pour qu'elle corresponde à celle renvoyée par getTasks().
  3. observeTasks : crée une coroutine à l'aide de runBlocking et exécute refreshTasks, puis renvoie observableTasks.

Vous trouverez ci-dessous le code de ces méthodes.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

Étape 4 : Ajouter une méthode de test pour addTasks

Lors des tests, il est préférable d'avoir déjà des Tasks dans votre dépôt. Vous pouvez appeler saveTask plusieurs fois, mais pour faciliter les choses, ajoutez une méthode d'assistance spécifiquement pour les tests qui vous permet d'ajouter des tâches.

  1. Ajoutez la méthode addTasks, qui accepte un vararg de tâches, ajoute chacune d'elles à HashMap, puis actualise les tâches.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

À ce stade, vous disposez d'un faux dépôt pour les tests, avec quelques-unes des méthodes clés implémentées. Ensuite, utilisez-le dans vos tests !

Dans cette tâche, vous allez utiliser une fausse classe dans un ViewModel. Utilisez l'injection de dépendances par constructeur pour intégrer les deux sources de données en ajoutant une variable TasksRepository au constructeur de TasksViewModel.

Ce processus est un peu différent avec les ViewModels, car vous ne les construisez pas directement. Exemple :

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


Comme dans le code ci-dessus, vous utilisez le délégué de propriété viewModel's qui crée le ViewModel. Pour modifier la façon dont le modèle de vue est construit, vous devez ajouter et utiliser un ViewModelProvider.Factory. Si vous ne connaissez pas ViewModelProvider.Factory, vous pouvez en savoir plus ici.

Étape 1 : Créer et utiliser une ViewModelFactory dans TasksViewModel

Commencez par mettre à jour les classes et les tests liés à l'écran Tasks.

  1. Ouvrez TasksViewModel.
  2. Modifiez le constructeur de TasksViewModel pour qu'il accepte TasksRepository au lieu de le construire à l'intérieur de la classe.

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

Comme vous avez modifié le constructeur, vous devez maintenant utiliser une fabrique pour construire TasksViewModel. Placez la classe factory dans le même fichier que TasksViewModel, mais vous pouvez également la placer dans son propre fichier.

  1. En bas du fichier TasksViewModel, en dehors de la classe, ajoutez un TasksViewModelFactory qui accepte un TasksRepository brut.

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


Il s'agit de la méthode standard pour modifier la façon dont les ViewModel sont construits. Maintenant que vous avez la fabrique, utilisez-la chaque fois que vous construisez votre ViewModel.

  1. Mettez à jour TasksFragment pour utiliser l'usine.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Exécutez le code de votre application et assurez-vous que tout fonctionne toujours.

Étape 2 : Utiliser FakeTestRepository dans TasksViewModelTest

Désormais, au lieu d'utiliser le vrai dépôt dans vos tests de ViewModel, vous pouvez utiliser le faux dépôt.

  1. Ouvrez TasksViewModelTest.
  2. Ajoutez une propriété FakeTestRepository dans TasksViewModelTest.

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. Mettez à jour la méthode setupViewModel pour créer un FakeTestRepository avec trois tâches, puis construisez le tasksViewModel avec ce dépôt.

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. Étant donné que vous n'utilisez plus le code AndroidX Test ApplicationProvider.getApplicationContext, vous pouvez également supprimer l'annotation @RunWith(AndroidJUnit4::class).
  2. Exécutez vos tests et assurez-vous qu'ils fonctionnent toujours tous.

En utilisant l'injection de dépendances du constructeur, vous avez supprimé DefaultTasksRepository en tant que dépendance et l'avez remplacé par votre FakeTestRepository dans les tests.

Étape 3 : Mettre à jour également le fragment et le ViewModel TaskDetail

Apportez exactement les mêmes modifications pour TaskDetailFragment et TaskDetailViewModel. Cela préparera le code pour la prochaine fois que vous écrirez des tests TaskDetail.

  1. Ouvrez TaskDetailViewModel.
  2. Mettez à jour le constructeur :

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. En bas du fichier TaskDetailViewModel, en dehors de la classe, ajoutez un TaskDetailViewModelFactory.

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. Mettez à jour TasksFragment pour utiliser l'usine.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Exécutez votre code et assurez-vous que tout fonctionne.

Vous pouvez désormais utiliser un FakeTestRepository au lieu du dépôt réel dans TasksFragment et TasksDetailFragment.

Vous allez ensuite écrire des tests d'intégration pour tester les interactions entre votre fragment et votre ViewModel. Vous saurez si le code de votre ViewModel met à jour correctement votre UI. Pour ce faire, vous utilisez

  • le modèle ServiceLocator
  • les bibliothèques Espresso et Mockito ;

Les tests d'intégration testent l'interaction de plusieurs classes pour s'assurer qu'elles se comportent comme prévu lorsqu'elles sont utilisées ensemble. Ces tests peuvent être exécutés localement (ensemble de sources test) ou en tant que tests d'instrumentation (ensemble de sources androidTest).

Dans votre cas, vous allez prendre chaque fragment et écrire des tests d'intégration pour le fragment et le ViewModel afin de tester les principales fonctionnalités du fragment.

Étape 1 : Ajouter des dépendances Gradle

  1. Ajoutez les dépendances Gradle suivantes.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

Voici quelques-unes de ces dépendances :

  • junit:junit : JUnit, nécessaire pour écrire des instructions de test de base.
  • androidx.test:core : bibliothèque de test AndroidX principale
  • kotlinx-coroutines-test : bibliothèque de test des coroutines
  • androidx.fragment:fragment-testing : bibliothèque de test AndroidX permettant de créer des fragments dans les tests et de modifier leur état.

Comme vous utiliserez ces bibliothèques dans votre ensemble de sources androidTest, utilisez androidTestImplementation pour les ajouter en tant que dépendances.

Étape 2 : Créer une classe TaskDetailFragmentTest

TaskDetailFragment affiche des informations sur une seule tâche.

Vous allez commencer par écrire un test de fragment pour TaskDetailFragment, car il possède des fonctionnalités assez basiques par rapport aux autres fragments.

  1. Ouvrez taskdetail.TaskDetailFragment.
  2. Générez un test pour TaskDetailFragment, comme vous l'avez fait précédemment. Acceptez les choix par défaut et placez-le dans l'ensemble de sources androidTest (et NON dans l'ensemble de sources test).

  1. Ajoutez les annotations suivantes à la classe TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

Les annotations ont pour but :

  • @MediumTest : marque le test comme test d'intégration à "durée d'exécution moyenne" (par rapport aux tests unitaires @SmallTest et aux tests de bout en bout de grande taille @LargeTest). Cela vous aide à regrouper et à choisir la taille du test à exécuter.
  • @RunWith(AndroidJUnit4::class) : utilisé dans n'importe quelle classe utilisant AndroidX Test.

Étape 3 : Lancer un fragment à partir d'un test

Dans cette tâche, vous allez lancer TaskDetailFragment à l'aide de la bibliothèque de test AndroidX. FragmentScenario est une classe d'AndroidX Test qui encapsule un fragment et vous permet de contrôler directement son cycle de vie pour les tests. Pour écrire des tests pour les fragments, vous créez un FragmentScenario pour le fragment que vous testez (TaskDetailFragment).

  1. Copiez ce test dans TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Ce code ci-dessus :

Ce n'est pas encore un test terminé, car il n'affirme rien. Pour l'instant, exécutez le test et observez ce qui se passe.

  1. Il s'agit d'un test instrumenté. Assurez-vous donc que l'émulateur ou votre appareil sont visibles.
  2. Exécutez le test.

Plusieurs choses devraient se produire.

  • Tout d'abord, comme il s'agit d'un test instrumenté, il s'exécutera sur votre appareil physique (s'il est connecté) ou sur un émulateur.
  • Le fragment devrait se lancer.
  • Notez qu'il ne navigue dans aucun autre fragment et n'a aucun menu associé à l'activité. Il s'agit uniquement du fragment.

Enfin, regardez attentivement et remarquez que le fragment indique "Aucune donnée" car il ne parvient pas à charger les données de la tâche.

Votre test doit à la fois charger TaskDetailFragment (ce que vous avez fait) et affirmer que les données ont été chargées correctement. Pourquoi n'y a-t-il aucune donnée ? En effet, vous avez créé une tâche, mais vous ne l'avez pas enregistrée dans le dépôt.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Vous disposez de ce FakeTestRepository, mais vous avez besoin d'un moyen de remplacer votre véritable dépôt par votre faux dépôt pour votre fragment. Vous le ferez ensuite.

Dans cette tâche, vous allez fournir votre faux dépôt à votre fragment à l'aide d'un ServiceLocator. Cela vous permettra d'écrire vos tests d'intégration de fragment et de ViewModel.

Vous ne pouvez pas utiliser l'injection de dépendances du constructeur ici, comme vous l'avez fait auparavant, lorsque vous deviez fournir une dépendance au ViewModel ou au dépôt. L'injection de dépendances du constructeur nécessite que vous construisiez la classe. Les fragments et les activités sont des exemples de classes que vous ne construisez pas et dont vous n'avez généralement pas accès au constructeur.

Comme vous ne construisez pas le fragment, vous ne pouvez pas utiliser l'injection de dépendances du constructeur pour remplacer le double de test du dépôt (FakeTestRepository) par le fragment. Utilisez plutôt le modèle Service Locator. Le modèle Service Locator est une alternative à l'injection de dépendances. Il s'agit de créer une classe singleton appelée "Service Locator", dont l'objectif est de fournir des dépendances, à la fois pour le code régulier et le code de test. Dans le code d'application standard (l'ensemble de sources main), toutes ces dépendances sont les dépendances d'application standards. Pour les tests, vous modifiez le localisateur de services afin de fournir des versions doubles de test des dépendances.

Ne pas utiliser le Service Locator


Utiliser un localisateur de services

Pour l'application de cet atelier de programmation, procédez comme suit :

  1. Créez une classe Service Locator capable de construire et de stocker un dépôt. Par défaut, il construit un dépôt "normal".
  2. Refactorisez votre code pour utiliser le localisateur de services lorsque vous avez besoin d'un dépôt.
  3. Dans votre classe de test, appelez une méthode sur le Service Locator qui remplace le dépôt "normal" par votre test double.

Étape 1 : Créer le ServiceLocator

Créons une classe ServiceLocator. Il résidera dans l'ensemble de sources principal avec le reste du code de l'application, car il est utilisé par le code de l'application principal.

Remarque : ServiceLocator est un singleton. Utilisez donc le mot clé Kotlin object pour la classe.

  1. Créez le fichier ServiceLocator.kt au niveau supérieur de l'ensemble de sources principal.
  2. Définissez un object appelé ServiceLocator.
  3. Créez des variables d'instance database et repository, et définissez-les toutes les deux sur null.
  4. Annoter le dépôt avec @Volatile, car il peut être utilisé par plusieurs threads (@Volatile est expliqué en détail ici).

Votre code doit ressembler à ce qui suit.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

Pour l'instant, la seule chose que votre ServiceLocator doit faire est de savoir comment renvoyer un TasksRepository. Il renverra un DefaultTasksRepository préexistant ou en créera un, si nécessaire.DefaultTasksRepository

Définissez les fonctions suivantes :

  1. provideTasksRepository : fournit un dépôt existant ou en crée un. Cette méthode doit être synchronized sur this pour éviter de créer accidentellement deux instances de dépôt dans les situations où plusieurs threads sont en cours d'exécution.
  2. createTasksRepository : code permettant de créer un dépôt. Appelle createTaskLocalDataSource et crée un TasksRemoteDataSource.
  3. createTaskLocalDataSource : code permettant de créer une source de données locale. Appellera le createDataBase.
  4. createDataBase : code permettant de créer une base de données.

Le code final est donné ci-dessous.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

Étape 2 : Utiliser ServiceLocator dans l'application

Vous allez modifier le code de votre application principale (et non vos tests) afin de créer le dépôt dans un seul endroit, votre ServiceLocator.

Il est important de ne créer qu'une seule instance de la classe de dépôt. Pour ce faire, vous allez utiliser le localisateur de service dans la classe Application.

  1. Au niveau supérieur de la hiérarchie de votre package, ouvrez TodoApplication et créez un val pour votre dépôt, puis attribuez-lui un dépôt obtenu à l'aide de ServiceLocator.provideTaskRepository.

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Maintenant que vous avez créé un dépôt dans l'application, vous pouvez supprimer l'ancienne méthode getRepository dans DefaultTasksRepository.

  1. Ouvrez DefaultTasksRepository et supprimez l'objet associé.

DefaultTasksRepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

Désormais, remplacez toutes les instances de getRepository par le taskRepository de l'application. Cela garantit que vous obtenez le dépôt fourni par ServiceLocator au lieu de créer directement le dépôt.

  1. Ouvrez TaskDetailFragement et recherchez l'appel à getRepository en haut de la classe.
  2. Remplacez cet appel par un appel qui récupère le dépôt à partir de TodoApplication.

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. Faites de même pour TasksFragment.

TasksFragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. Pour StatisticsViewModel et AddEditTaskViewModel, mettez à jour le code qui acquiert le dépôt pour utiliser le dépôt à partir de TodoApplication.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Exécutez votre application (et non le test).

Étant donné que vous n'avez fait que refactoriser, l'application devrait s'exécuter de la même manière, sans problème.

Étape 3 : Create FakeAndroidTestRepository

Vous avez déjà un FakeTestRepository dans l'ensemble de sources de test. Par défaut, vous ne pouvez pas partager de classes de test entre les ensembles de sources test et androidTest. Vous devez donc créer une classe FakeTestRepository en double dans l'ensemble de sources androidTest et l'appeler FakeAndroidTestRepository.

  1. Effectuez un clic droit sur l'ensemble de sources androidTest et créez un package de données. Effectuez à nouveau un clic droit et créez un package source .
  2. Créez une classe appelée FakeAndroidTestRepository.kt dans ce package source.
  3. Copiez le code suivant dans cette classe.

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

Étape 4 : Préparer votre ServiceLocator pour les tests

OK, il est temps d'utiliser ServiceLocator pour remplacer les doubles de test lors des tests. Pour ce faire, vous devez ajouter du code à votre code ServiceLocator.

  1. Ouvrez ServiceLocator.kt.
  2. Marquez le setter pour tasksRepository comme @VisibleForTesting. Cette annotation permet d'indiquer que le setter est public à des fins de test.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Que vous exécutiez votre test seul ou dans un groupe de tests, il devrait se dérouler exactement de la même manière. Cela signifie que vos tests ne doivent pas avoir de comportement dépendant les uns des autres (ce qui implique d'éviter de partager des objets entre les tests).

Étant donné que ServiceLocator est un singleton, il peut être partagé accidentellement entre les tests. Pour éviter cela, créez une méthode qui réinitialise correctement l'état ServiceLocator entre les tests.

  1. Ajoutez une variable d'instance appelée lock avec la valeur Any.

ServiceLocator.kt

private val lock = Any()
  1. Ajoutez une méthode spécifique aux tests appelée resetRepository, qui efface la base de données et définit le dépôt et la base de données sur "null".

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

Étape 5 : Utiliser votre ServiceLocator

Dans cette étape, vous utilisez ServiceLocator.

  1. Ouvrez TaskDetailFragmentTest.
  2. Déclarez une variable lateinit TasksRepository.
  3. Ajoutez une méthode de configuration et de nettoyage pour configurer un FakeAndroidTestRepository avant chaque test et le nettoyer après chaque test.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Encapsulez le corps de la fonction activeTaskDetails_DisplayedInUi() dans runBlockingTest.
  2. Enregistrez activeTask dans le dépôt avant de lancer le fragment.
repository.saveTask(activeTask)

Le test final ressemble au code ci-dessous.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. Annotez l'ensemble de la classe avec @ExperimentalCoroutinesApi.

Une fois terminé, le code se présentera comme suit.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. Exécutez le test activeTaskDetails_DisplayedInUi().

Comme avant, vous devriez voir le fragment, sauf que cette fois, comme vous avez correctement configuré le dépôt, il affiche les informations de la tâche.


Dans cette étape, vous allez utiliser la bibliothèque de test d'interface utilisateur Espresso pour effectuer votre premier test d'intégration. Vous avez structuré votre code de manière à pouvoir ajouter des tests avec des assertions pour votre UI. Pour ce faire, vous utiliserez la bibliothèque de test Espresso.

Espresso vous aide à :

  • Interagissez avec les vues, par exemple en cliquant sur des boutons, en faisant glisser une barre ou en faisant défiler un écran vers le bas.
  • Affirmez que certaines vues sont à l'écran ou dans un certain état (par exemple, qu'elles contiennent un texte particulier ou qu'une case à cocher est cochée, etc.).

Étape 1 : Remarque sur la dépendance Gradle

Vous disposerez déjà de la dépendance Espresso principale, car elle est incluse par défaut dans les projets Android.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core : cette dépendance Espresso principale est incluse par défaut lorsque vous créez un projet Android. Il contient le code de test de base pour la plupart des vues et des actions qui y sont associées.

Étape 2 : Désactiver les animations

Les tests Espresso s'exécutent sur un appareil réel et sont donc des tests d'instrumentation par nature. Les animations peuvent poser problème : si une animation est lente et que vous essayez de tester si une vue est à l'écran, mais qu'elle est toujours en cours d'animation, Espresso peut échouer accidentellement à un test. Cela peut rendre les tests Espresso instables.

Pour les tests d'UI Espresso, nous vous recommandons de désactiver les animations (votre test s'exécutera également plus rapidement) :

  1. Sur votre appareil de test, accédez à Paramètres > Options pour les développeurs.
  2. Désactivez les trois paramètres suivants : Échelle d'animation des fenêtres, Échelle d'animation des transitions et Échelle de durée d'animation.

Étape 3 : Examiner un test Espresso

Avant d'écrire un test Espresso, examinez du code Espresso.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

Cette instruction recherche la vue de la case à cocher avec l'ID task_detail_complete_checkbox, clique dessus, puis affirme qu'elle est cochée.

La majorité des instructions Espresso sont constituées de quatre parties :

1. Méthode Espresso statique

onView

onView est un exemple de méthode Espresso statique qui démarre une instruction Espresso. onView est l'une des plus courantes, mais il existe d'autres options, comme onData.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId est un exemple de ViewMatcher qui récupère une vue par son ID. Vous trouverez d'autres correspondances de vues dans la documentation.

3. ViewAction

perform(click())

La méthode perform qui utilise un ViewAction. Une ViewAction est une action qui peut être effectuée sur la vue. Dans cet exemple, il s'agit d'un clic sur la vue.

4. ViewAssertion

check(matches(isChecked()))

check qui prend un ViewAssertion. Les ViewAssertions vérifient ou affirment quelque chose concernant la vue. L'assertion matches est la plus courante des ViewAssertion que vous utiliserez. Pour terminer l'assertion, utilisez un autre ViewMatcher, en l'occurrence isChecked.

Notez que vous n'appelez pas toujours perform et check dans une instruction Espresso. Vous pouvez avoir des instructions qui ne font qu'une assertion à l'aide de check ou qui ne font qu'un ViewAction à l'aide de perform.

  1. Ouvrez TaskDetailFragmentTest.kt.
  2. Mettez à jour le test activeTaskDetails_DisplayedInUi.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Voici les instructions d'importation, si nécessaire :

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. Tout ce qui suit le commentaire // THEN utilise Espresso. Examinez la structure du test et l'utilisation de withId, puis vérifiez les assertions sur l'apparence de la page de détails.
  2. Exécutez le test et vérifiez qu'il réussit.

Étape 4 : Facultatif : Écrire votre propre test Espresso

Écrivez maintenant un test vous-même.

  1. Créez un test appelé completedTaskDetails_DisplayedInUi et copiez-y ce code squelette.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. En vous basant sur le test précédent, terminez ce test.
  2. Exécutez le test et vérifiez qu'il réussit.

Une fois terminé, le code completedTaskDetails_DisplayedInUi doit ressembler à ceci :

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

Dans cette dernière étape, vous allez apprendre à tester le composant Navigation à l'aide d'un autre type de test double appelé mock et de la bibliothèque de test Mockito.

Dans cet atelier de programmation, vous avez utilisé un double de test appelé "fake". Les fakes sont l'un des nombreux types de doubles de test. Quel test double devez-vous utiliser pour tester le composant Navigation ?

Réfléchissez à la façon dont la navigation se déroule. Imaginez que vous appuyez sur l'une des tâches de TasksFragment pour accéder à l'écran d'informations sur la tâche.

Voici le code dans TasksFragment qui permet d'accéder à l'écran d'informations d'une tâche lorsque l'utilisateur appuie dessus.

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


La navigation se produit en raison d'un appel à la méthode navigate. Si vous deviez écrire une instruction assert, il n'existe pas de moyen simple de tester si vous avez accédé à TaskDetailFragment. La navigation est une action complexe qui n'entraîne pas de résultat ni de changement d'état clairs, au-delà de l'initialisation de TaskDetailFragment.

Vous pouvez affirmer que la méthode navigate a été appelée avec le paramètre d'action approprié. C'est exactement ce que fait un mock : il vérifie si des méthodes spécifiques ont été appelées.

Mockito est un framework permettant de créer des doubles de test. Bien que le mot "mock" soit utilisé dans l'API et le nom, il ne sert pas uniquement à créer des mocks. Il peut également créer des stubs et des espions.

Vous utiliserez Mockito pour créer un mock NavigationController qui peut affirmer que la méthode de navigation a été appelée correctement.

Étape 1 : Ajouter des dépendances Gradle

  1. Ajoutez les dépendances Gradle.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core : il s'agit de la dépendance Mockito.
  • dexmaker-mockito : cette bibliothèque est requise pour utiliser Mockito dans un projet Android. Mockito doit générer des classes au moment de l'exécution. Sur Android, cela se fait à l'aide du bytecode dex. Cette bibliothèque permet donc à Mockito de générer des objets lors de l'exécution sur Android.
  • androidx.test.espresso:espresso-contrib : cette bibliothèque est composée de contributions externes (d'où son nom) qui contiennent du code de test pour des vues plus avancées, telles que DatePicker et RecyclerView. Il contient également des vérifications d'accessibilité et une classe appelée CountingIdlingResource, qui sera abordée plus tard.

Étape 2 : Créer TasksFragmentTest

  1. Ouvrez TasksFragment.
  2. Effectuez un clic droit sur le nom de la classe TasksFragment, puis sélectionnez Generate (Générer), puis Test (Tester). Créez un test dans l'ensemble de sources androidTest.
  3. Copiez ce code dans le TasksFragmentTest.

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

Ce code ressemble à celui de TaskDetailFragmentTest que vous avez écrit. Il configure et supprime un FakeAndroidTestRepository. Ajoutez un test de navigation pour vérifier que lorsque vous cliquez sur une tâche dans la liste des tâches, vous êtes redirigé vers le bon TaskDetailFragment.

  1. Ajoutez le test clickTask_navigateToDetailFragmentOne.

TasksFragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Utilisez la fonction mock de Mockito pour créer un mock.

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

Pour simuler dans Mockito, transmettez la classe que vous souhaitez simuler.

Ensuite, vous devez associer votre NavController au fragment. onFragment vous permet d'appeler des méthodes sur le fragment lui-même.

  1. Définissez votre nouveau mock comme NavController du fragment.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Ajoutez le code pour cliquer sur l'élément du RecyclerView dont le texte est "TITLE1".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions fait partie de la bibliothèque espresso-contrib et vous permet d'effectuer des actions Espresso sur un RecyclerView.

  1. Vérifiez que navigate a été appelé avec le bon argument.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

La méthode verify de Mockito est ce qui fait de cette classe un mock. Vous pouvez confirmer que le navController simulé a appelé une méthode spécifique (navigate) avec un paramètre (actionTasksFragmentToTaskDetailFragment avec l'ID "id1").

Le test complet se présente comme suit :

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Exécutez le test.

En résumé, pour tester la navigation, vous pouvez :

  1. Utilisez Mockito pour créer un mock NavController.
  2. Associez ce NavController simulé au fragment.
  3. Vérifiez que la navigation a été appelée avec l'action et le ou les paramètres corrects.

Étape 3 : Facultatif, écrivez clickAddTaskButton_navigateToAddEditFragment

Pour voir si vous pouvez écrire vous-même un test de navigation, essayez cette tâche.

  1. Écrivez le test clickAddTaskButton_navigateToAddEditFragment qui vérifie que si vous cliquez sur le bouton d'action flottant +, vous êtes redirigé vers AddEditTaskFragment.

La réponse se trouve ci-dessous.

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

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_2


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 vous a montré comment configurer l'injection de dépendances manuelle et un localisateur de services, et comment utiliser des faux et des mocks dans vos applications Android Kotlin. En particulier :

  • Le type de test que vous allez implémenter pour votre application dépend de ce que vous souhaitez tester et de votre stratégie de test. Les tests unitaires sont ciblés et rapides. Les tests d'intégration vérifient l'interaction entre les différentes parties de votre programme. Les tests de bout en bout permettent de valider les fonctionnalités. Ils sont très fidèles, sont souvent instrumentés et peuvent prendre plus de temps à s'exécuter.
  • L'architecture de votre application a une incidence sur la difficulté des tests.
  • Le développement piloté par les tests (TDD, Test Driven Development) est une stratégie qui consiste à écrire les tests en premier, puis à créer la fonctionnalité pour réussir les tests.
  • Pour isoler des parties de votre application à des fins de test, vous pouvez utiliser des doubles de test. Un test double est une version d'une classe conçue spécifiquement pour les tests. Par exemple, vous simulez l'obtention de données à partir d'une base de données ou d'Internet.
  • Utilisez l'injection de dépendances pour remplacer une classe réelle par une classe de test, par exemple un dépôt ou une couche réseau.
  • Utilisez les tests instrumentés (androidTest) pour lancer les composants d'UI.
  • Lorsque vous ne pouvez pas utiliser l'injection de dépendances du constructeur, par exemple pour lancer un fragment, vous pouvez souvent utiliser un localisateur de services. Le modèle de localisation de services est une alternative à l'injection de dépendances. Il s'agit de créer une classe singleton appelée "Service Locator", dont l'objectif est de fournir des dépendances, à la fois pour le code régulier et le code de test.

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