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:
- Le langage de programmation Kotlin
- Concepts de test abordés dans le premier atelier de programmation: écriture et exécution de tests unitaires sur Android, avec JUnit, Hamcrest, test AndroidX, Robolectric et test de LiveData
- Les principales bibliothèques Android Jetpack suivantes:
ViewModel
,LiveData
et le composant de navigation - Architecture d'application, basée sur le modèle du Guide de l'architecture des applications et Ateliers de programmation Android Fundamentals
- Les bases des coroutines sur Android
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:
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:
- Atelier de programmation Room with a View
- Ateliers de programmation Android Kotlin Fundamentals
- Ateliers de programmation Android avancés
- Exemple de tournesol Android
- Cours de développement d'applications Android avec Kotlin Udacity
Il est plus important de comprendre l'architecture générale de l'application que de maîtriser la logique à n'importe quelle couche.
Voici le résumé des packages que vous trouverez:
Paquet : | |
| Écran d'ajout ou de modification d'une tâche:code du calque d'interface utilisateur permettant d'ajouter ou de modifier une tâche. |
| Couche de données : ce calque gère la couche de données des tâches. Il contient la base de données, le réseau et le code du dépôt. |
| Écran de statistiques : code de l'interface utilisateur correspondant à l'écran des statistiques. |
| Écran des détails de la tâche:code de l'interface utilisateur correspondant à une seule tâche. |
| Écran de tâches:code de l'interface utilisateur pour la liste de toutes les tâches. |
| Cours pratiques : cours partagés utilisés dans différentes sections de l'application (par exemple, pour la mise en page "Balayer l'écran" sur plusieurs écrans). |
Couche de données (.data)
Cette application inclut une couche réseau simulée, dans le package remote et une couche de base de données, dans le package local. Pour plus de simplicité, dans ce projet, la couche réseau est simulée avec un simple HashMap
avec du retard, plutôt que d'effectuer des requêtes réseau réelles.
Les coordonnées DefaultTasksRepository
ou les médiateurs entre la couche réseau et la couche de base de données sont les valeurs qui sont renvoyées à la couche UI.
Couche d'interface utilisateur ( .addedittask, .statistics, .taskdetail, .tasks)
Chacun des packages de la couche de l'interface utilisateur contient un fragment, un modèle de vue et toutes les autres classes requises pour l'interface utilisateur (par exemple, un adaptateur pour la liste de tâches). L'TaskActivity
correspond à l'activité contenant tous les fragments.
Navigation
La navigation dans l'application est contrôlée par le composant Navigation. Elle est définie dans le fichier nav_graph.xml
. La navigation est déclenchée dans les modèles de vue à l'aide de la classe Event
. Les modèles de vue déterminent également les arguments à transmettre. Les fragments observent les Event
et effectuent la navigation réelle entre les écrans.
Dans 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:
- Tout d'abord, vous testez unitaire le dépôt.
- 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.
- Vous allez ensuite apprendre à écrire des tests d'intégration pour les fragments et leurs modèles de vue.
- 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 |
Dummy | Double de test transmis sans être utilisé, par exemple si vous souhaitez simplement le fournir en tant que paramètre Si vous aviez |
Espion | Un test de double, qui effectue aussi le suivi de certaines informations supplémentaires. Par exemple, si vous avez créé un |
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
.
- Dans l'ensemble de sources test, effectuez un clic droit sur New -> Package (Nouveau -> Package).
- Créez un package data contenant un package source.
- 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
.
- 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 { ... }
- Faites en sorte que
FakeDataSource
implémenteTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio se plaint que vous n'avez pas implémenté les méthodes requises pour TasksDataSource
.
- Utilisez le menu de correction rapide, puis sélectionnez Implémenter les membres.
- 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.
- Modifiez le constructeur
FakeDataSource
pour créer unvar
appelétasks
qui est de typeMutableList<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:
getTasks
: sitasks
n'est pasnull
, renvoie un résultatSuccess
. Sitasks
estnull
, renvoyez un résultatError
.- Écrire
deleteAllTasks
: efface la liste des tâches modifiables. - É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
- Modifiez le constructeur
DefaultTaskRepository
pour remplacerApplication
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 }
- Comme vous avez transmis les dépendances, supprimez la méthode
init
. Vous n'avez plus besoin de créer les dépendances. - 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
- 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
.
- Effectuez un clic droit sur le nom de la classe
DefaultTasksRepository
, sélectionnez Generate (Générer), puis Test. - Suivez les instructions pour créer
DefaultTasksRepositoryTest
dans l'ensemble de la source test. - 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 }
- Créez trois variables, deux variables
FakeDataSource
membre (une pour chaque source de données pour votre dépôt) et une variable pour laDefaultTasksRepository
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
.
- Créez une méthode appelée
createRepository
et annotez-la avec@Before
. - Instanciez de fausses sources de données à l'aide des listes
remoteTasks
etlocalTasks
. - Instanciez votre
tasksRepository
à l'aide des deux fausses sources de données que vous venez de créer et deDispatchers.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
!
- Écrivez un test pour la méthode
getTasks
du dépôt. Vérifiez que lorsque vous appelezgetTasks
avectrue
(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.
- 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.
- 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. - Revenez dans
DefaultTasksRepositoryTest
, ajoutezrunBlockingTest
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))
}
}
- 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
.
- Ouvrez
DefaultTasksRepository
, puis effectuez un clic droit sur le nom de la classe. Sélectionnez ensuite Refactor -> Extract -> Interface (Refactoriser - & gt; Extraire - & gt; Interface).
- Sélectionnez Extraire dans un fichier séparé.
- Dans la fenêtre Extraire l'interface, remplacez le nom de l'interface par
TasksRepository
. - Dans la section Membres du formulaire, cochez tous les membres, sauf les deux membres associés et les méthodes privées.
- Cliquez sur Refactor (Refactoriser). La nouvelle interface
TasksRepository
doit apparaître dans le package data/source.
DefaultTasksRepository
implémente désormais TasksRepository
.
- 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.
- 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'interfaceTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
Vous serez invité à mettre en œuvre les méthodes d'interface.
- Passez la souris sur l'erreur jusqu'à ce que le menu de suggestions s'affiche, puis cliquez sur Implement members (Implémenter des membres).
- 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.
- Dans
FakeTestRepository
, ajoutez à la fois une variableLinkedHashMap
représentant la liste actuelle de tâches et une variableMutableLiveData
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:
getTasks
: cette méthode doit utilisertasksServiceData
pour la transformer en liste à l'aide detasksServiceData.values.toList()
, puis renvoyer le résultat en tant que résultatSuccess
.refreshTasks
: met à jour la valeur deobservableTasks
pour obtenir la valeur renvoyée pargetTasks()
.observeTasks
: crée une coroutine à l'aide derunBlocking
et exécuterefreshTasks
, puis renvoieobservableTasks
.
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.
- Ajoutez la méthode
addTasks
, qui utilise unevararg
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
.
- Ouvrez
TasksViewModel
. - Modifiez le constructeur de
TasksViewModel
de sorte qu'il utiliseTasksRepository
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.
- En bas du fichier
TasksViewModel
, en dehors de la classe, ajoutez unTasksViewModelFactory
qui prend uneTasksRepository
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.
- 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))
}
- 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.
- Ouvrez
TasksViewModelTest
. - Ajoutez une propriété
FakeTestRepository
dans le fichierTasksViewModelTest
.
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
}
- Mettez à jour la méthode
setupViewModel
pour créer unFakeTestRepository
avec trois tâches, puis créez letasksViewModel
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)
}
- Comme vous n'utilisez plus le code AndroidX Test
ApplicationProvider.getApplicationContext
, vous pouvez également supprimer l'annotation@RunWith(AndroidJUnit4::class)
. - 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
.
- Ouvrez
TaskDetailViewModel
. - 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 }
- En bas du fichier
TaskDetailViewModel
, ajoutez unTaskDetailViewModelFactory
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)
}
- 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))
}
- 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
- 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 AndroidXkotlinx-coroutines-test
: bibliothèque de tests de coroutinesandroidx.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.
- Ouvrez
taskdetail.TaskDetailFragment
. - 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 sourcestest
).
- 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
).
- 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:
- Crée une tâche.
- Un
Bundle
est créé. Il représente les arguments de fragment de la tâche transmise au fragment. - La fonction
launchFragmentInContainer
crée unFragmentScenario
, avec ce groupe et un thème.
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.
- Ce test étant instrumenté, assurez-vous de l'émulateur ou de votre appareil.
- 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:
- 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".
- Refactorisez votre code de sorte que, lorsque vous avez besoin d'un dépôt, utilisez l'outil de localisation de services.
- 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.
- Créez le fichier ServiceLocator.kt au premier niveau de l'ensemble de la source principale.
- Définissez un
object
appeléServiceLocator
. - Créez des variables d'instance
database
etrepository
, puis définissez-les surnull
. - Annotez le dépôt avec
@Volatile
, car il pourrait être utilisé par plusieurs threads (@Volatile
explique 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:
provideTasksRepository
: fournit un dépôt existant ou en crée un. Cette méthode doit êtresynchronized
surthis
pour éviter les cas où plusieurs threads s'exécutent, et ne créent jamais accidentellement deux instances de dépôt.createTasksRepository
: code permettant de créer un dépôt. appellecreateTaskLocalDataSource
et crée unTasksRemoteDataSource
.createTaskLocalDataSource
: code permettant de créer une source de données locale. Appel decreateDataBase
.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.
- Au niveau supérieur de la hiérarchie de vos packages, ouvrez
TodoApplication
et créez unval
pour votre dépôt, puis attribuez-lui un dépôt obtenu à l'aide deServiceLocator.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
.
- 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
.
- Ouvrez
TaskDetailFragement
et recherchez l'appel àgetRepository
en haut de la classe. - 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)
}
- 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)
}
- Pour
StatisticsViewModel
etAddEditTaskViewModel
, mettez à jour le code qui acquiert le dépôt afin qu'il utilise le dépôt deTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- 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
.
- 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. - Créez une classe appelée
FakeAndroidTestRepository.kt
dans ce package source. - 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
.
- Ouvrez
ServiceLocator.kt
. - 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.
- Ajoutez une variable d'instance appelée
lock
avec la valeurAny
.
ServiceLocator.kt
private val lock = Any()
- 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
.
- Ouvrez
TaskDetailFragmentTest
. - Déclarez une variable
lateinit TasksRepository
. - 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()
}
- Encapsulez le corps de la fonction de
activeTaskDetails_DisplayedInUi()
dansrunBlockingTest
. - 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)
}
- 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)
}
}
- 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).
- Sur votre appareil de test, accédez à Paramètres > Options pour les développeurs.
- 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:
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).
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
.
- Ouvrez
TaskDetailFragmentTest.kt
. - 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
- Tous les éléments publiés après le commentaire
// THEN
utilisent Espresso. Passez en revue la structure de test et l'utilisation dewithId
, puis vérifiez que la page d'informations comporte des assertions. - 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.
- 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
}
- Si vous regardez le test précédent, effectuez ce test.
- 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
- 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 queDatePicker
etRecyclerView
. Il contient également des tests d'accessibilité et une classe appeléeCountingIdlingResource
, que nous aborderons plus tard.
Étape 2. Créer un TasksFragmentTest
- Ouvrez
TasksFragment
. - 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. - 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
.
- 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)
}
- 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.
- Composez la nouvelle simulation
NavController
pour le nouveau fragment.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- 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.
- 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")
)
}
- Exécutez votre test !
En résumé, procédez comme suit pour tester la navigation:
- Utilisez Mockito pour créer une simulation
NavController
. - Associez la simulation
NavController
au fragment. - 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.
- É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.
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:
- Guide de l'architecture des applications
runBlocking
etrunBlockingTest
FragmentScenario
- Expresso
- Mockito
- JUnit 4
- Bibliothèque de tests AndroidX
- Bibliothèque principale des composants d'architecture AndroidX
- Ensembles de sources
- Effectuer un test à partir de la ligne de commande
Vidéos :
Autre :
Pour obtenir des liens vers d'autres ateliers de programmation dans ce cours, consultez la page de destination "Avancé Android" dans les ateliers de programmation Kotlin.