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

Cet atelier de programmation fait partie du cours "Advanced Android" en langage Kotlin. Vous tirerez pleinement parti de ce cours si vous suivez les ateliers en séquence, mais ce n'est pas obligatoire. Tous les ateliers de programmation du cours sont répertoriés sur la page de destination des ateliers de programmation Android avancés sur Kotlin.

Introduction

Ce deuxième atelier de programmation porte sur les doubles de test: quand les utiliser sous Android et comment les mettre en œuvre à l'aide de l'injection de dépendances, du modèle de localisation de services et des bibliothèques. Vous apprendrez ainsi à écrire:

  • Tests unitaires du dépôt
  • Fragments et tests d'intégration de modèles de vue
  • Tests de navigation fragment

Ce que vous devez déjà savoir

Vous devez être au fait:

Points abordés

  • Planifier une stratégie de test
  • Comment créer et utiliser des doubles de test, à savoir des contrefaçons et des simulations
  • Utiliser l'injection de dépendances manuelle sur Android pour les tests unitaires et d'intégration
  • Appliquer le schéma de localisation de services
  • Tester les dépôts, les fragments, l'affichage des modèles et le composant de 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 en utilisant un double test et une injection de dépendances.
  • Écrivez des tests unitaires pour un modèle de vue en utilisant un double test et une injection de dépendances.
  • Écrivez des tests d'intégration pour des fragments et leurs modèles de vue à l'aide du framework de test de l'interface utilisateur Espresso.
  • Rédigez des tests de navigation à l'aide de Mockito et d'Espresso.

Dans cette série d'ateliers de programmation, vous travaillerez avec l'application NOTES À FAIRE. Elle vous permet de noter des tâches à effectuer et de les afficher sous forme de liste. Vous pouvez ensuite les marquer comme terminées ou non, les filtrer ou les supprimer.

Cette application est codée en Kotlin, dispose de plusieurs écrans et utilise des composants Jetpack. Elle suit l'architecture d'un guide sur l'architecture des applications. Apprenez à tester cette application pour tester les applications qui utilisent les mêmes bibliothèques et la même architecture.

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écuter l'exemple d'application

Une fois l'application À faire installée, ouvrez-la dans Android Studio et exécutez-la. Il devrait se compiler. Explorez l'application en procédant comme suit:

  • Créez une tâche avec le bouton d'action flottant plus. Saisissez d'abord un titre, puis des informations supplémentaires sur la tâche. Enregistrez-le avec le bouton d'action flottant vert.
  • Dans la liste des tâches, cliquez sur leur titre, puis consultez l'écran détaillé de la tâche pour afficher le reste de la description.
  • Dans la liste ou sur l'écran des détails, cochez la case correspondant à cette tâche pour définir son état sur Terminé.
  • Revenez à l'écran des tâches, ouvrez le menu des filtres et filtrez les tâches par Active et Terminé.
  • Ouvrez le panneau de navigation, puis cliquez sur Statistiques.
  • Revenez à l'écran de présentation, puis, dans le menu du panneau de navigation, sélectionnez Effacer les tâches terminées pour supprimer toutes les tâches dont l'état est Terminée.

Étape 2: Explorez l'exemple de code de l'application

L'application À FAIRE repose sur l'exemple de test et d'architecture d'architecture Plans d'architecture populaire (qui utilise la version architecture réactive de l'exemple). L'application suit l'architecture d'un guide sur l'architecture des applications. Il utilise ViewModel avec des fragments, un dépôt et une salle. Si vous connaissez l'un des exemples ci-dessous, l'application a une architecture similaire:

Il est plus important de comprendre l'architecture générale de l'application que de maîtriser la logique à n'importe quelle couche.

Voici le résumé des packages que vous trouverez:

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

.addedittask

Écran d'ajout ou de modification d'une tâche:code du calque d'interface utilisateur permettant d'ajouter ou de modifier une tâche.

.data

Couche de données : ce calque gère la couche de données des tâches. Il contient la base de données, le réseau et le code du dépôt.

.statistics

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

.taskdetail

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

.tasks

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

.util

Cours pratiques : cours partagés utilisés dans différentes sections de l'application (par exemple, pour la mise en page "Balayer l'écran" sur plusieurs écrans).

Couche de données (.data)

Cette application inclut une couche réseau simulée, dans le package remote et une couche de base de données, dans le package local. Pour plus de simplicité, dans ce projet, la couche réseau est simulée avec un simple HashMap avec du retard, plutôt que d'effectuer des requêtes réseau réelles.

Les coordonnées DefaultTasksRepository ou les médiateurs entre la couche réseau et la couche de base de données sont les valeurs qui sont renvoyées à la couche UI.

Couche d'interface utilisateur ( .addedittask, .statistics, .taskdetail, .tasks)

Chacun des packages de la couche de l'interface utilisateur contient un fragment, un modèle de vue et toutes les autres classes requises pour l'interface utilisateur (par exemple, un adaptateur pour la liste de tâches). L'TaskActivity correspond à l'activité contenant tous les fragments.

Navigation

La navigation dans l'application est contrôlée par le composant Navigation. Elle est définie dans le fichier nav_graph.xml. La navigation est déclenchée dans les modèles de vue à l'aide de la classe Event. Les modèles de vue déterminent également les arguments à transmettre. Les fragments observent les Event et effectuent la navigation réelle entre les écrans.

Dans cet atelier de programmation, vous allez apprendre à tester des dépôts, à afficher des modèles et des fragments à l'aide des doubles de test et de l'injection de dépendances. Avant d'étudier ces notions, il est important de comprendre les raisons qui vous guideront et de quelle manière vous allez écrire ces tests.

Cette section présente certaines bonnes pratiques à suivre pour effectuer des tests en général, car elles s'appliquent à Android.

Pyramide de test

Une stratégie de test repose sur trois aspects:

  • Portée : quelle part du code le test touche-t-il ? Les tests peuvent s'exécuter sur une seule méthode, sur l'ensemble de l'application ou ailleurs.
  • Vitesse : quelle est la vitesse du test ? Les vitesses de test peuvent varier de quelques millisecondes à plusieurs minutes.
  • Fidelity : dans quelle mesure le monde réel est-il testé ? Par exemple, si une partie du code que vous testez doit envoyer une requête réseau, le code de test effectue-t-il réellement cette requête réseau ou fausse-t-il le résultat ? Si le test communique avec le réseau, cela signifie une fidélité plus élevée. Le seul inconvénient est que le test peut prendre plus de temps, et que des erreurs se produisent en cas d'interruption du réseau, ou son utilisation peut être coûteuse.

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, généralement moins fidèle, et inversement. Voici trois méthodes courantes permettant de répartir les tests automatisés:

  • Tests unitaires : tests très ciblés qui s'exécutent sur une seule classe, généralement une seule méthode dans cette classe. Si un test unitaire échoue, vous pouvez savoir exactement où se trouve le problème dans votre code. En effet, la fidélité de votre application est bien plus faible, car en conditions réelles, votre application implique bien plus que l'exécution d'une méthode ou d'une classe. Elles sont suffisamment rapides pour s'exécuter chaque fois que vous modifiez votre code. Elles seront le plus souvent exécutées en local (dans l'ensemble de sources test). Exemple: Tester des méthodes uniques pour afficher les modèles et les dépôts
  • Tests d'intégration : ces tests testent l'interaction de plusieurs classes afin de s'assurer qu'elles se comportent comme prévu lorsqu'elles sont utilisées ensemble. Pour structurer les tests d'intégration, vous pouvez leur demander de tester une seule fonctionnalité, par exemple la possibilité d'enregistrer une tâche. Ils testent un champ d'application de code plus large que les tests unitaires, mais sont toujours optimisés pour une exécution rapide plutôt que pour une fidélité totale. Ils peuvent être exécutés en local ou en tant que tests d'instrumentation, en fonction de la situation. Exemple : Test de toutes les fonctionnalités d'une seule paire de modèles de fragment et de vue.
  • Tests de bout en bout : testez une combinaison de fonctionnalités. Ils testent de grandes parties de l'application, simulent étroitement l'utilisation réelle et sont donc généralement lents. Ils offrent une fidélité maximale et vous indiquent que votre application fonctionne dans son ensemble. Dans l'ensemble, ces tests seront des tests instrumentés (dans l'ensemble de sources androidTest)
    Exemple: Démarrer l'application dans son ensemble et tester quelques fonctionnalités ensemble.

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

Architecture et tests

Votre capacité à tester votre application à différents niveaux de la pyramide de test est étroitement liée à l'architecture de votre application. Par exemple, une application extrêmement mal conçue peut intégrer toute sa logique dans une méthode. Dans la mesure où ces tests testent généralement de grandes parties de l'application, vous pouvez écrire un test de bout en bout. 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 associé à une seule unité ou à une seule fonctionnalité.

Une meilleure approche consisterait à décomposer la logique d'application en plusieurs méthodes et classes, afin de tester chaque élément de manière isolée. L'architecture permet de diviser et d'organiser votre code, ce qui facilite les tests unitaires et d'intégration. L'application À FAIRE que vous allez tester suit une architecture particulière :



Dans cette leçon, vous allez apprendre à tester des parties de l'architecture ci-dessus, de façon isolée:

  1. Tout d'abord, vous testez unitaire le dépôt.
  2. Vous utiliserez ensuite un double test dans le modèle de vue, ce qui est nécessaire pour effectuer des tests unitaires et des tests d'intégration.
  3. Vous allez ensuite apprendre à écrire des tests d'intégration pour les fragments et leurs modèles de vue.
  4. Pour finir, vous allez apprendre à écrire des tests d'intégration incluant le composant de navigation.

Nous aborderons ce sujet de bout en bout dans la leçon suivante.

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

Tester uniquement le code d'une ou de plusieurs classes peut s'avérer délicat. 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. Il s'agit de la classe pour laquelle vous écrirez des tests unitaires.

Votre objectif est de tester uniquement le code de cette classe. Cependant, DefaultTaskRepository dépend d'autres classes, comme LocalTaskDataSource et RemoteTaskDataSource, pour fonctionner. Autre exemple : 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 les autres classes pour enregistrer des informations dans une base de données ou communiquer avec le réseau.



Prenons l'exemple de 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 de base les plus courants vers votre dépôt. Cette méthode inclut la lecture d'une base de données SQLite et les appels de réseau (appel à updateTasksFromRemoteDataSource). Cela implique beaucoup plus de code que seulement le code du dépôt.

Voici quelques raisons plus spécifiques qui peuvent expliquer la difficulté de tester le dépôt:

  • Vous devez gérer la création et la gestion d'une base de données pour réaliser les tests les plus simples pour ce dépôt. Vous voyez apparaître des questions telles que "un test local ou instrumenté" et, si vous utilisez AndroidX Test, pour obtenir un environnement Android simulé.
  • Certaines parties du code, telles que le code de réseau, peuvent prendre beaucoup de temps ou, parfois, entraîner des échecs de création de tests de longue durée.
  • Vos tests pourraient ne plus être capables de diagnostiquer le code responsable d'un échec de test. Vos tests peuvent commencer à tester du code hors dépôt. Par exemple, ces tests pourraient échouer en raison d'un problème dans une partie du code dépendant, tel que le code de la base de données.

Tester les doubles

La solution à cela est que lorsque vous testez le dépôt, n'utilisez pas le véritable réseau ou le code de la base de données, mais utilisez plutôt un double test. Un double de test est une version d'une classe conçue spécialement à des fins de test. Cette fonctionnalité a pour but de remplacer la version réelle d'une classe dans les tests. Ce type de cascades est semblable à celui d'un double, et il est spécialisé dans les cascades. Il remplace le vrai acteur dans des actes dangereux.

Voici quelques types de doubles de test:

Faux

Un test de test qui a une "mise en œuvre" pour la classe, mais qui est implémenté pour y parvenir lors des tests, mais non adapté à la production.

Maquette

Un test double permettant de savoir quelles méthodes ont été appelées. Ce test réussit ou échoue, selon qu'il a été appelé correctement ou non.

Stub

Un double test sans logique et qui ne renvoie que ce que vous programmez à renvoyer. Un StubTaskRepository peut être programmé pour renvoyer certaines combinaisons de tâches de getTasks, par exemple.

Dummy

Double de test transmis sans être utilisé, par exemple si vous souhaitez simplement le fournir en tant que paramètre Si vous aviez NoOpTaskRepository, il s'agirait simplement d'implémenter le TaskRepository sans code sans dans les méthodes.

Espion

Un test de double, qui effectue aussi le suivi de 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 Tester les toilettes de test.

Les doubles de test les plus courants sur Android sont les faux et les fausses.

Dans cette tâche, vous allez créer un test FakeDataSource double pour un test unitaire DefaultTasksRepository dissocié des sources de données réelles.

Étape 1: Créer la classe FakeDataSource

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

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

  1. Créez un package data contenant un package source.
  2. Créez une classe appelée FakeDataSource dans le package data/source.

Étape 2: Implémentez l'interface TasksDataSource

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

  1. Notez que ces deux éléments 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 se plaint que vous n'avez pas implémenté les méthodes requises pour TasksDataSource.

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


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

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

FakeDataSource est un type de double test spécifique, appelé faux. Un faux est un double qui a une "mise en œuvre" et un « travail » d'une classe, mais qui est mis en œuvre de manière à être adaptée aux tests, mais pas adapté à la production. L'implémentation signifie que la classe produira des résultats réalistes en fonction des entrées.

Par exemple, votre fausse source de données ne se connecte pas au réseau et n'enregistre aucun élément dans une base de données. Elle utilise simplement une liste en mémoire. Cela fonctionnera comme prévu avec ces méthodes d'obtention ou d'enregistrement des tâches, mais elles renverront les résultats attendus, mais vous ne pourrez jamais utiliser cette implémentation en production, car elle n'est enregistrée ni sur le serveur, ni sur la base de données.

FakeDataSource

  • vous permet de tester le code dans DefaultTasksRepository sans dépendre d'une véritable base de données ou d'un véritable réseau.
  • Fournit une mise en œuvre des tests en conditions réelles.
  1. Modifiez le constructeur FakeDataSource pour créer un var appelé tasks qui est de type MutableList<Task>? et dont la valeur par défaut est une liste vide modifiable.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Voici la liste des tâches qui arrivent sur une base de données ou une réponse du serveur. Pour l'instant, l'objectif est de tester la méthode repository'sgetTasks. Cela appelle les méthodes source de données getTasks, deleteAllTasks et saveTask.

Rédigez une fausse version de ces méthodes:

  1. getTasks: si tasks n'est pas null, renvoie un résultat Success. Si tasks est null, renvoyez un résultat Error.
  2. Écrire deleteAllTasks: efface la liste des tâches modifiables.
  3. Écrire en saveTask : ajoute 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

Ce fonctionnement est semblable à celui des sources de données locales et distantes réelles.

Au cours de cette étape, vous allez utiliser une technique appelée "injection de dépendances manuelle" afin de pouvoir utiliser le double test que vous venez de créer.

Le principal problème est qu'il existe une FakeDataSource, mais son utilisation 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 non la dépendance à ce type d'exécution.

Pour le moment, les dépendances sont créées 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
}

Comme vous créez et attribuez des sous-titres taskLocalDataSource et tasksRemoteDataSource dans DefaultTasksRepository, ils sont essentiellement codés en dur. Il n'est pas possible de permuter deux fois votre test.

À la place, vous devez fournir ces sources de données à la classe au lieu de les coder en dur. L'injection 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 de permuter le double du test en le transmettant au constructeur.

Pas d'injection

Injection

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

  1. Modifiez le constructeur DefaultTaskRepository pour remplacer Application par des sources de données et par le coordinateur de coroutine (qui vous devrez également échanger pour vos tests ; cela est décrit plus en détail dans la troisième section sur les coroutines).

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 du constructeur !

Étape 2: Utilisez FakeDataSource dans vos tests

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

  1. Effectuez un clic droit sur le nom de la classe DefaultTasksRepository, sélectionnez Generate (Générer), puis Test.
  2. Suivez les instructions pour créer DefaultTasksRepositoryTest dans l'ensemble de la source test.
  3. En haut de votre nouvelle classe DefaultTasksRepositoryTest, ajoutez les variables de membre ci-dessous pour représenter les données dans 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 FakeDataSource membre (une pour chaque source de données pour votre dépôt) et une variable pour la 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. Votre DefaultTasksRepository utilisera votre double de test, FakeDataSource.

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

La méthode finale doit ressembler à ce qui suit :

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 DefaultTasksRepository getTasks()

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 être actualisé à partir de la source de données distante), cela signifie qu'il affiche des données de la source de données distante (et non de 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 si vous appelez getTasks:

Étape 4: Ajoutez 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'une portée de coroutine. Pour résoudre cette erreur, vous devrez 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 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. Cette fonction est fournie par la bibliothèque de test de coroutines. Elle utilise 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 : les actions sont effectuées dans un ordre déterministe. Cela signifie que vos coroutines s'exécutent comme des non-coroutines. Il est donc destiné à tester du code.

Utilisez runBlockingTest dans vos classes de test lorsque vous appelez une fonction suspend. Vous en apprendrez davantage sur le fonctionnement de runBlockingTest, ainsi que sur le test des coroutines dans le prochain atelier de programmation de cette série.

  1. Ajoutez le @ExperimentalCoroutinesApi au-dessus de la classe. Cela implique que vous savez que vous utilisez une API de coroutine expérimentale (runBlockingTest) dans la classe. Sinon, vous recevrez un avertissement.
  2. Revenez dans DefaultTasksRepositoryTest, ajoutez runBlockingTest afin qu'il soit pris en compte dans votre test sous la forme d'un code

Ce dernier test 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 s'est produite.

Vous venez de voir comment effectuer un test unitaire pour un référentiel. Au cours des étapes suivantes, vous allez à nouveau utiliser l'injection de dépendances et créer un autre test deux fois, cette fois pour démontrer comment écrire les 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. C'est ce que l'on appelle faire des isolations dans lesquelles vous isolez clairement votre unité et ne testez que le code de cette unité.

Ainsi, TasksViewModelTest ne doit tester que le code TasksViewModel. Il ne doit pas tester les classes de base de données, de réseau ou de dépôt. Par conséquent, pour vos modèles de vue, 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 une injection de dépendances pour afficher les modèles.

Étape 1 : Créer une interface TasksRepository

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

Comment cela fonctionne-t-il concrètement ? Regardez TasksRemoteDataSource, TasksLocalDataSource et FakeDataSource. Notez qu'ils partagent la même interface : TasksDataSource. Cela vous permet d'indiquer dans le constructeur de DefaultTasksRepository que vous prenez dans un TasksDataSource.

DefaultTasksRepository.kt

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

Cela nous permet de permuter 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 publique de l'API) de DefaultTasksRepository.

  1. Ouvrez DefaultTasksRepository, puis effectuez un clic droit sur le nom de la classe. Sélectionnez ensuite Refactor -> Extract -> Interface (Refactoriser - & gt; Extraire - & gt; Interface).

  1. Sélectionnez Extraire dans un fichier séparé.

  1. Dans la fenêtre Extraire l'interface, remplacez le nom de l'interface par TasksRepository.
  2. Dans la section Membres du formulaire, cochez tous les membres, sauf les deux membres associés et les méthodes privées.


  1. Cliquez sur Refactor (Refactoriser). La nouvelle interface TasksRepository doit 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 un FakeTestRepository

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

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

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Vous serez invité à mettre en œuvre les méthodes d'interface.

  1. Passez la souris sur l'erreur jusqu'à ce que le menu de suggestions s'affiche, puis cliquez sur Implement members (Implémenter des membres).
  1. Sélectionnez toutes les méthodes et appuyez sur OK.

Étape 3. Implémenter des méthodes FakeTestRepository

Vous disposez à présent d'une classe FakeTestRepository avec des méthodes non implémentées. Comme pour l'implémentation de FakeDataSource, le FakeTestRepository s'appuie sur une structure de données au lieu de gérer une médiation compliquée entre des sources de données locales et distantes.

Notez que votre FakeTestRepository n'a pas besoin d'utiliser des FakeDataSource ni rien d'autre. Il suffit de renvoyer des sorties fictives réalistes en fonction des entrées. Vous utiliserez un LinkedHashMap pour stocker la liste des tâches, ainsi qu'un MutableLiveData pour vos tâches observables.

  1. Dans FakeTestRepository, ajoutez à la fois une variable LinkedHashMap représentant la liste actuelle de tâches et une variable 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
}

Appliquez les méthodes suivantes:

  1. getTasks : cette méthode doit utiliser tasksServiceData pour la transformer en liste à l'aide de tasksServiceData.values.toList(), puis renvoyer le résultat en tant que résultat Success.
  2. refreshTasks : met à jour la valeur de observableTasks pour obtenir la valeur 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 ajouter des tâches

Lors des tests, il est préférable d'avoir déjà Tasks dans votre dépôt. Vous pouvez appeler saveTask plusieurs fois. Toutefois, pour faciliter cet ajout, ajoutez une méthode d'assistance spécifique aux tests qui vous permettent d'ajouter des tâches.

  1. Ajoutez la méthode addTasks, qui utilise une vararg de tâches, ajoute chacune à 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 tester différentes méthodes clés. Vous pouvez ensuite l'utiliser dans vos tests.

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

Ce processus est légèrement différent avec les modèles de vue, car vous ne les créez 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 la propriété viewModel's qui crée le modèle de vue. Pour modifier la construction du modèle de vue, vous devez ajouter et utiliser un ViewModelProvider.Factory. Si vous ne connaissez pas ViewModelProvider.Factory, cliquez ici pour en savoir plus.

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

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

  1. Ouvrez TasksViewModel.
  2. Modifiez le constructeur de TasksViewModel de sorte qu'il utilise TasksRepository au lieu de le construire dans 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 
}

Puisque vous avez modifié le constructeur, vous devez maintenant utiliser une fabrique pour construire TasksViewModel. Indiquez la classe Factory dans le même fichier que la classe 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 prend une TasksRepository simple.

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 construction des ViewModel. Maintenant que vous avez la fabrique, utilisez-la partout où vous créez votre modèle de vue.

  1. Mettez à jour TasksFragment pour utiliser la configuration d'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 app et assurez-vous que tout fonctionne toujours.

Étape 2. Utiliser FakeTestRepository dans TasksViewModelTest

Au lieu d'utiliser le véritable dépôt dans vos tests de modèles de vue, vous pouvez utiliser le dépôt fictif.

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

TâcheVueModèleTest.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 créez le tasksViewModel avec ce dépôt.

TâchesViewModelTest.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. Comme 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.

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

Étape 3. Mettre également à jour le fragment TaskDetails et le ViewModel

Apportez les mêmes modifications à TaskDetailFragment et TaskDetailViewModel. Vous pourrez ainsi préparer le code à écrire lors des prochains tests TaskDetail.

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

TâcheDetailsViewModel.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, ajoutez un TaskDetailViewModelFactory en dehors du cours.

TâcheDetailsViewModel.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 la configuration d'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 vérifiez que tout fonctionne correctement.

Vous pouvez maintenant utiliser un FakeTestRepository au lieu du véritable dépôt dans TasksFragment et TasksDetailFragment.

Ensuite, vous allez écrire des tests d'intégration pour tester votre fragment et afficher vos interactions avec le modèle. Vous apprendrez si le code du modèle de vue met à jour votre UI de manière appropriée. Pour ce faire, vous utilisez

  • le modèle ServiceLocator.
  • Bibliothèques Espresso et Mockito

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

Dans le cas présent, vous allez utiliser chaque fragment et écrire des tests d'intégration pour le fragment et le modèle de vue afin de tester les principales caractéristiques 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"

Ces dépendances incluent les éléments suivants:

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

Puisque vous allez utiliser 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 TaskDetailsFragmentTest

Le TaskDetailFragment affiche les informations sur une seule tâche.

Vous commencerez par écrire un test de fragment pour TaskDetailFragment, car il possède des fonctionnalités relativement 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-les dans l'ensemble de sources androidTest (et NON dans l'ensemble de sources test).

  1. Ajoutez les annotations suivantes à la classe TaskDetailFragmentTest.

TâcheDétailsFragmentTest.kt

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

}

Cette annotation a pour fonction:

  • @MediumTest : marque le test comme un test d'intégration "moyen de temps d'exécution" (par opposition aux tests unitaires @SmallTest et aux tests @LargeTest de grande envergure). Cela vous permet de regrouper et de choisir la taille du test à exécuter.
  • @RunWith(AndroidJUnit4::class) : utilisé dans n'importe quelle classe utilisant Android 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 le cycle de vie du fragment à des fins de test. Pour écrire des tests sur des fragments, créez un fragment FragmentScenario pour le fragment que vous testez (TaskDetailFragment).

  1. Copiez ce test dans TaskDetailFragmentTest.

TâcheDétailsFragmentTest.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 test n'est pas encore terminé, car il n'a aucune déclaration à effectuer. Pour l'instant, exécutez le test et observez ce qui se passe.

  1. Ce test étant instrumenté, assurez-vous de l'émulateur ou de votre appareil.
  2. Exécutez le test.

Plusieurs actions devraient être nécessaires.

  • Tout d'abord, comme il s'agit d'un test instrumenté, le test s'exécutera sur votre appareil physique (s'il est connecté) ou sur un émulateur.
  • Il doit lancer le fragment.
  • Vous remarquerez qu'il ne "parcourt" aucun autre fragment ni qu'aucun menu n'est associé à l'activité : il s'agit uniquement du fragment.

Enfin, observez bien que le fragment indique "Pas de données", car il ne charge pas les données de la tâche.

Votre test doit charger le TaskDetailFragment (ce que vous avez fait) et affirmer que les données ont été chargées correctement. Pourquoi n'y a-t-il pas de données ? 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 cet FakeTestRepository, mais vous avez besoin d'un moyen de remplacer votre véritable dépôt par le faux créé pour votre fragment. Ce que vous devrez faire ensuite !

Dans cette tâche, vous allez fournir votre référentiel fictif à votre fragment à l'aide d'un ServiceLocator. Vous pourrez ainsi écrire votre fragment et afficher les tests d'intégration de modèle.

Vous ne pouvez pas utiliser l'injection de dépendances du constructeur ici, comme vous l'avez fait précédemment, lorsque vous devez fournir une dépendance au modèle de vue ou au dépôt. Vous devez créer la classe pour l'injection de dépendances. Les fragments et les activités sont des exemples de classes dont vous n'avez pas l'accès et que vous n'avez généralement pas accès.

Étant donné que vous ne créez pas le fragment, vous ne pouvez pas utiliser l'injection de dépendances du constructeur pour remplacer le double du test du dépôt (FakeTestRepository) par le fragment. Utilisez plutôt le schéma Service de localisation. 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 le but est de fournir des dépendances, à la fois pour le code standard et le code de test. Dans le code d'application standard (l'ensemble source main), toutes ces dépendances sont les dépendances d'application standards. Modifiez les localisations de service pour tester les doubles versions des dépendances.

Utiliser l'outil de localisation de services


Utiliser un outil de localisation de services

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

  1. Créer une classe de localisation de services capable de créer et de stocker un dépôt. Par défaut, il crée un dépôt "normal".
  2. Refactorisez votre code de sorte que, lorsque vous avez besoin d'un dépôt, utilisez l'outil de localisation de services.
  3. Dans votre classe de test, appelez une méthode sur l'outil de localisation de services qui remplace le dépôt ""normal" par le double de votre test.

Étape 1 : Créer le ServiceLocator

Nous allons créer une classe ServiceLocator. Il figurera dans le code source principal, avec le reste du code de l'application, car il est utilisé par le code de l'application principale.

Remarque: Le ServiceLocator est un singleton. Vous devez donc utiliser le mot clé object Kotlin pour la classe.

  1. Créez le fichier ServiceLocator.kt au premier niveau de l'ensemble de la source principale.
  2. Définissez un object appelé ServiceLocator.
  3. Créez des variables d'instance database et repository, puis définissez-les sur null.
  4. Annotez le dépôt avec @Volatile, car il pourrait être utilisé par plusieurs threads (@Volatileexplique ici cette page).

Le code doit se présenter comme suit.

object ServiceLocator {

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

}

Pour le moment, la seule chose à faire pour ServiceLocator est de savoir comment renvoyer un TasksRepository. Il renvoie un objet DefaultTasksRepository existant ou génère un nouvel objet DefaultTasksRepository, si nécessaire.

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 les cas où plusieurs threads s'exécutent, et ne créent jamais accidentellement deux instances de dépôt.
  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. Appel de 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 depuis un seul emplacement, votre ServiceLocator.

Il est important de ne créer qu'une seule instance de la classe Repository. Pour cela, vous allez utiliser l'outil de localisation de services de ma classe Application.

  1. Au niveau supérieur de la hiérarchie de vos packages, 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
            }
        }
    }
}

Maintenant, partout où vous utilisiez getRepository, utilisez plutôt le taskRepository de l'application. Ainsi, au lieu de créer le dépôt directement, vous obtiendrez n'importe quel dépôt fourni par ServiceLocator.

  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 depuis TodoApplication.

TaskDétailsFragment.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 la même chose 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 afin qu'il utilise le dépôt 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).

Comme vous avez uniquement refactorisé l'application, celle-ci doit fonctionner sans problème.

Étape 3. Créer un FakeAndroidTestRepository

Vous avez déjà un FakeTestRepository défini dans la source de test. Par défaut, vous ne pouvez pas partager des 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, puis l'appeler FakeAndroidTestRepository.

  1. Effectuez un clic droit sur l'ensemble de sources androidTest et créez un package data. Effectuez un nouveau 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

Il est temps d'utiliser la ServiceLocator pour intervertir le test et doubler pendant le test. 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'exprimer que la setter est publique en raison de tests.

ServiceLocator.kt

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

Que vous exécutiez votre test seul ou dans un groupe, vous devez réaliser les mêmes tests. En d'autres termes, vous devez éviter que les tests aient des comportements interdépendants, ce qui signifie que vous ne devez pas partager d'objets entre différents tests.

Étant donné que le ServiceLocator est un singleton, il peut être partagé accidentellement entre des 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 à la fois 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 service Locator

Au cours de cette étape, vous allez utiliser ServiceLocator.

  1. Ouvrez TaskDetailFragmentTest.
  2. Déclarez une variable lateinit TasksRepository.
  3. Ajoutez une configuration et une méthode de suppression après FakeAndroidTestRepository pour chaque test, puis nettoyez-les après chaque test.

TâcheDétailsFragmentTest.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 de activeTaskDetails_DisplayedInUi() dans runBlockingTest.
  2. Enregistrez activeTask dans le dépôt avant de lancer le fragment.
repository.saveTask(activeTask)

Le test final ressemble à ce code ci-dessous.

TâcheDétailsFragmentTest.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 toute la classe avec @ExperimentalCoroutinesApi.

Une fois l'opération terminée, le code se présente comme suit :

TâcheDétailsFragmentTest.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 auparavant, vous devriez voir le fragment, sauf que cette fois, étant donné que vous avez correctement configuré le dépôt, il affiche désormais les informations de la tâche.


Au cours de cette étape, vous devrez utiliser la bibliothèque de tests de l'UI d'Espresso pour effectuer votre premier test d'intégration. Vous avez structuré votre code de façon à pouvoir ajouter des tests avec des assertions pour votre interface utilisateur. Pour ce faire, vous devez utiliser la bibliothèque de tests Espresso.

Espresso vous aide à:

  • interagir avec les vues, par exemple en cliquant sur des boutons, en faisant glisser une barre ou en faisant défiler l'écran vers le bas ;
  • Confirmez que certaines vues sont affichées à l'écran ou dans un état donné (par exemple, si elles contiennent un texte particulier ou si une case est cochée).

Étape 1 : Noter la dépendance Gradle

Vous possédez déjà 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 lors de la création d'un projet Android. Il contient le code de test de base pour la plupart des vues et des actions correspondantes.

Étape 2. Désactiver les animations

Les tests à expresso s'exécutent sur un appareil réel et sont donc des tests d'instrumentation par nature. L'animation des animations est le problème suivant: si une animation prend du retard et que vous essayez de tester si une vue s'affiche à l'écran, mais que l'animation s'exécute, l'expresso peut échouer accidentellement. Ce procédé peut irrégulier.

Pour tester les interfaces utilisateur, il est recommandé de désactiver les animations (votre test sera également plus rapide).

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

Étape 3. Un test à l'expresso

Avant de rédiger un test Express, examinez certains codes Espresso.

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

Cette instruction permet de détecter la case à cocher associée à l'ID task_detail_complete_checkbox, de cliquer dessus, puis d'affirmer qu'elle est cochée.

La majorité des déclarations en présence d'expresso comportent quatre parties:

1. Méthode Expresss

onView

onView est un exemple de méthode Expresss qui démarre une instruction Express. onView est l'une des options les plus courantes, mais il existe d'autres options, par exemple onData.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId est un exemple de ViewMatcher qui obtient une vue grâce à son ID. Vous pouvez trouver d'autres outils de mise en correspondance des vues dans la documentation.

3. ViewAction.

perform(click())

La méthode perform, qui utilise un ViewAction. Une opération ViewAction peut être effectuée sur la vue (par exemple, pour cliquer sur celle-ci).

4. ViewAssertion

check(matches(isChecked()))

check, qui prend un ViewAssertion. L'élément ViewAssertion vérifie ou affirme quelque chose à propos de la vue. L'argument ViewAssertion le plus courant que vous utiliserez est l'assertion matches. Pour finaliser l'assertion, utilisez un autre ViewMatcher, dans ce cas isChecked.

Notez que vous n'appelez pas toujours perform et check dans une instruction Express. Vous pouvez utiliser des instructions qui font simplement une assertion à l'aide de check ou simplement une ViewAction à l'aide de perform.

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

TâcheDétailsFragmentTest.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. Tous les éléments publiés après le commentaire // THEN utilisent Espresso. Passez en revue la structure de test et l'utilisation de withId, puis vérifiez que la page d'informations comporte des assertions.
  2. Exécutez le test et confirmez qu'il a été réussi.

Étape 4 : Facultatif : Rédigez votre propre test Express

Maintenant, rédigez vous-même un test.

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

TâcheDétailsFragmentTest.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. Si vous regardez le test précédent, effectuez ce test.
  2. Exécutez l'exécution du test et confirmez-le.

Le fichier completedTaskDetails_DisplayedInUi terminé doit ressembler à ce code.

TâcheDétailsFragmentTest.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()))
    }

Lors de la dernière étape, vous allez apprendre à tester le composant de 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é "faux". Il s'agit de l'un des nombreux types de doubles de tests. Quel double test devez-vous utiliser pour tester le composant de navigation ?

Tenez compte de la navigation. Imaginez que vous appuyez sur l'une des tâches dans TasksFragment pour accéder à un écran de détails de tâche.

Code ici dans TasksFragment qui permet d'accéder à un écran d'informations sur 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 est effectuée en raison d'un appel à la méthode navigate. Vous n'avez pas la possibilité de rédiger une déclaration de déclaration pour savoir si vous avez accédé à TaskDetailFragment. La navigation est une action compliquée qui n'entraîne pas un résultat clair ni un changement d'état, au-delà de l'initialisation de TaskDetailFragment.

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

Mockito est un framework permettant de tester les doubles. Le mot fictif est utilisé dans l'API et dans le nom, mais pas non pour créer des simulations. Elle peut également faire des bouchons et des épices.

Vous allez utiliser Mockito pour créer une simulation NavigationController qui confirmera que la méthode de navigation a été appelée correctement.

Étape 1 : Ajouter des dépendances Gradle

  1. Ajoutez les dépendances de 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 de Mockito.
  • dexmaker-mockito : cette bibliothèque est nécessaire pour utiliser Mockito dans un projet Android. Mockito doit générer des classes au moment de l'exécution. Cette opération est effectuée sous Android et du code d'octets dex. Cette bibliothèque permet donc à Mockito de générer des objets pendant l'exécution sur Android.
  • androidx.test.espresso:espresso-contrib : cette bibliothèque est composée de contributions externes (d'où le nom) contenant du code de test pour des vues plus avancées, telles que DatePicker et RecyclerView. Il contient également des tests d'accessibilité et une classe appelée CountingIdlingResource, que nous aborderons plus tard.

Étape 2. Créer un TasksFragmentTest

  1. Ouvrez TasksFragment.
  2. Effectuez un clic droit sur le nom de la classe TasksFragment, sélectionnez Generate (Générer), puis Test. Créez un test dans l'ensemble de sources androidTest.
  3. Copiez ce code dans 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 au code TaskDetailFragmentTest que vous avez rédigé. 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, elle vous redirige vers le bon TaskDetailFragment.

  1. Ajoutez l'clickTask_navigateToDetailFragmentOne de test.

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 des simulations pour créer une simulation.

TasksFragmentTest.kt

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

Pour vous imiter à Mockito, passez la leçon à simuler.

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

  1. Composez la nouvelle simulation NavController pour le nouveau fragment.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Ajoutez le code pour cliquer sur l'élément situé dans le RecyclerView contenant le texte &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 RecyclerView.

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

C'est la méthode verify que nous utilisons pour simuler l'imitation de navController. Vous pouvez alors valider la simulation navController appelée une méthode spécifique (navigate) avec un paramètre (actionTasksFragmentToTaskDetailFragment, dont l'ID est &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 votre test !

En résumé, procédez comme suit pour tester la navigation:

  1. Utilisez Mockito pour créer une simulation NavController.
  2. Associez la simulation NavController au fragment.
  3. Assurez-vous que la navigation a été appelée avec les actions et les paramètres appropriés.

Étape 3. Facultatif : Écrivez clickAddTaskButton_naviToAddEditFragmentFragment

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 + FAB, vous accédez à AddEditTaskFragment.

La réponse est 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 afficher la différence entre le code initial et le code final.

Pour télécharger le code de l'atelier de programmation terminé, vous pouvez utiliser la commande Git ci-dessous:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_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 explique comment configurer manuellement l'injection de dépendances, un outil de localisation de services et comment utiliser les contrefaçons et les simulations dans vos applications Kotlin Android. Tenez particulièrement compte des points suivants :

  • Les tests à effectuer et votre stratégie de test déterminent les types de tests que vous allez mettre en œuvre pour votre application. Les tests unitaires sont ciblés et rapides. Les tests d'intégration permettent de vérifier les interactions entre les différentes parties de votre programme. Les tests de bout en bout permettent de vérifier que les fonctionnalités ont la plus haute fidélité, qu'elles sont souvent instrumentées et que leur exécution peut prendre plus de temps.
  • L'architecture de votre application influence la difficulté des tests.
  • Une stratégie de développement test/basé sur les tests est la stratégie qui consiste à créer les tests en premier, puis à créer la fonctionnalité pour les réussir.
  • Pour isoler des parties de votre application à des fins de test, vous pouvez utiliser des doublons. Un double de test est une version d'une classe conçue spécialement à des fins de test. Par exemple, vous adressez des données fictives à 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 intrusifs (androidTest) pour lancer les composants de l'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 outil de localisation 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 le but est de fournir des dépendances, à la fois pour le code standard et le code de test.

Cours Udacity:

Documentation pour les développeurs Android:

Vidéos :

Autre :

Pour obtenir des liens vers d'autres ateliers de programmation dans ce cours, consultez la page de destination "Avancé Android" dans les ateliers de programmation Kotlin.