Tester des requêtes réseau

1. Avant de commencer

Dans les précédents ateliers de programmation, vous avez appris comment créer des requêtes réseau à l'aide de Retrofit. Dans cet atelier, vous allez découvrir comment écrire des tests unitaires pour votre code.

Conditions préalables

  • Vous avez créé des répertoires de test dans Android Studio.
  • Vous avez écrit des tests unitaires et des tests d'instrumentation dans Android Studio.
  • Vous avez ajouté des dépendances Gradle à un projet Android.

Points abordés

  • Comment stocker des ressources pour les tests
  • Comment simuler des réponses d'API à des fins de test
  • Comment tester les services de l'API Retrofit

Ce dont vous avez besoin

  • Un ordinateur sur lequel est installé Android Studio
  • Le code de solution de l'application Mars Photos

Télécharger le code de démarrage pour cet atelier de programmation

Dans cet atelier, vous allez ajouter des tests d'instrumentation à l'application Mars Photos à partir du code de solution précédent.

Pour obtenir le code de cet atelier et l'ouvrir dans Android Studio, procédez comme suit :

Obtenir le code

  1. Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
  2. Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.

5b0a76c50478a73f.png

  1. Dans la boîte de dialogue, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
  2. Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
  3. Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.

Ouvrir le projet dans Android Studio

  1. Lancez Android Studio.
  2. Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).

36cc44fcf0f89a1d.png

Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).

21f3eec988dcfbe9.png

  1. Dans la boîte de dialogue Import Project (Importer un projet), accédez à l'emplacement du dossier du projet décompressé. Il se trouve probablement dans le dossier Téléchargements.
  2. Double-cliquez sur le dossier de ce projet.
  3. Attendez qu'Android Studio ouvre le projet.
  4. Cliquez sur le bouton Run (Exécuter) 11c34fc5e516fb1c.png pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
  5. Parcourez les fichiers du projet dans la fenêtre d'outil Project (Projet) pour voir comment l'application est configurée.

2. Présentation de l'application de démarrage

L'application Mars Photos se compose d'un seul écran qui affiche une liste de photos extraites à l'aide d'une requête réseau.

Ce code de démarrage présente des aspects supplémentaires permettant de faciliter les tests. Nous n'allons pas les examiner plus en détail, car ils n'entrent pas dans le cadre de cet atelier de programmation. Il est toutefois important de les mentionner.

Lorsque vous testez la manière dont une application gère la récupération de données à partir d'une API, il est toujours préférable de fournir vos propres données. De cette façon, vous savez à quoi elles doivent ressembler. Les données provenant d'une API peuvent être modifiées, ce qui peut interrompre les tests et les faire échouer inutilement. De plus, en se basant sur un appel réseau réel, des défaillances dues à la connectivité ou à la vitesse du réseau peuvent survenir, ce qui peut fausser les tests. Nous allons donc générer des données au cours de nos tests et les stocker en tant que fichier JSON dans le répertoire test/res. Il s'agit d'un répertoire de ressources semblable à celui du code de l'application principale, à la différence que nos ressources de test sont stockées dans test/res.

  1. Ajoutez le répertoire test/res. Dans la vue du projet, effectuez un clic droit sur le répertoire src, puis sélectionnez New > Directory (Nouveau > Répertoire) dans le menu déroulant.

95e789a6a8d34185.png

  1. Dans la fenêtre pop-up qui s'affiche, faites défiler la page vers le bas, puis sélectionnez test/res.

eeb4ef7904e60846.png

  1. Dans la vue du projet, effectuez un clic droit sur le répertoire test/res, puis sélectionnez New > File (Nouveau > Fichier).

6ea89527d6534c1a.png

  1. Attribuez le nom suivant au fichier : mars_photos.json.

c4f463255956ce33.png

  1. Copiez le code ci-dessous et collez-le dans le fichier mars_photos.json.
[
 {
   "id":"424905",
   "img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000MR0044631300503690E01_DXXX.jpg"
 },
 {
   "id":"424906",
   "img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
 }
]
  1. Pour que les fichiers de ce répertoire de ressources soient accessibles, le répertoire de ressources de test doit être explicitement spécifié comme répertoire "source" dans le fichier de compilation. Ajoutez la ligne suivante :

app/build.gradle

android {
    ...
    sourceSets {
       test.resources.srcDirs += 'src/test/res'
    }
}

Cela permet d'accéder aux fichiers de ressources sans avoir à saisir le chemin d'accès complet du fichier dans le code chaque fois que nous y accédons lors d'un test. De plus, saisir le chemin d'accès complet n'est pas une bonne pratique dans le cadre de tests, car il peut varier en fonction de l'appareil et du système d'exploitation.

  1. Dans le même fichier, ajoutez les dépendances ci-dessous et synchronisez Gradle.

app/build.gradle

dependencies {
    ...
    testImplementation 'junit:junit:4.12'
    testImplementation "androidx.arch.core:core-testing:2.1.0"
    testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
}
  1. À présent, créez un répertoire de test standard.

22137bac57bd2d77.png

  1. Dans le répertoire de test, créez un package com.example.android.marsphotos.
  2. Dans le package, créez un fichier Kotlin appelé BaseTest.kt.

481f7a26f0935093.png

  1. Copiez le code ci-dessous et collez-le dans le fichier BaseTest.kt. Si ce code vous semble étrange, ce n'est pas grave. Le terme "Mock" (Fictif) y apparaît à plusieurs reprises, et nous en parlerons plus loin dans cet atelier de programmation. La fonction enqueue() de cette classe analyse les données du fichier mars_photos.json que vous avez créé afin de pouvoir les utiliser dans un test que vous écrirez ultérieurement.

BaseTest.kt

import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source

open class BaseTest {

   val mockWebServer = MockWebServer()

   fun enqueue(fileName: String) {
       val inputStream = javaClass.classLoader!!.getResourceAsStream(fileName)
       val buffer = inputStream.source().buffer()

       mockWebServer.enqueue(
           MockResponse()
               .setResponseCode(200)
               .setBody(buffer.readString(Charsets.UTF_8))
       )
   }
}

3. Simulation de données

Dans cet atelier de programmation, nous nous appuyons beaucoup sur la simulation de données. Dans le cadre d'un test, il s'agit d'une technique qui consiste à simuler la valeur renvoyée par un morceau de code. Dans un atelier précédent, nous avons simulé une classe. Nous pouvons également simuler des fonctions et leur demander de renvoyer des valeurs que nous spécifions, ou simuler une API pour renvoyer une donnée que nous spécifions. C'est une solution pratique pour faire des tests, car elle permet d'isoler un morceau de code pour le tester. Dans cet atelier de programmation, nous allons nous concentrer sur la simulation d'une réponse d'API. Nous aborderons les fonctions de simulation dans un autre atelier.

4. Créer une classe de test unitaire

Dans ce test, nous allons tester le service d'API. Commencez par créer une classe appelée MarsApiServiceTests.kt.

5. Dépendances

Le code de démarrage de cet atelier de programmation contient les dépendances dont vous avez besoin. Cependant, l'une de ces dépendances mérite davantage d'explications.

testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"

Cette dépendance permet de créer un serveur fictif, lequel intercepte les requêtes réseau et les redirige pour renvoyer les données fictives que nous avons définies. Plus loin dans cet atelier, nous expliquerons en quoi consiste la simulation.

6. Bonnes pratiques

Notez que nos tests sont écrits en Kotlin, un langage de programmation orienté objet. Cela signifie que nous pouvons écrire du code orienté objet pour nos tests. Avec la quantité de code que nous allons écrire pour ce test, il n'est pas nécessaire d'utiliser des pratiques orientées objet. Cependant, nous allons tout de même les mettre en œuvre pour illustrer le concept.

Vous avez peut-être remarqué que le code de démarrage contient déjà un répertoire de test unitaire et qu'il contient une classe appelée BaseTest. Si nous disposons d'un extrait de code que nous pouvons utiliser pour plusieurs tests, il est possible de créer une open class (classe ouverte) et faire en sorte que nos classes de test en héritent. Gardez à l'esprit que cette classe BaseTest ne comporte qu'une seule méthode appelée enqueue(), et que nous n'écrirons qu'une unique classe de test dans cet atelier de programmation. Lorsque vous écrivez vos propres tests, sachez que vous pouvez utiliser la programmation orientée objet à votre avantage. Toutefois, il est important de ne pas en abuser si ce n'est pas nécessaire.

7. Écrire un test de requête réseau

Tout d'abord, assurez-vous que votre classe de test hérite de BaseTest.

Nous allons tester MarsApiService directement, donc nous avons besoin d'une instance de ce service. Nous allons la configurer de la même manière que dans le code de l'application, mais l'URL de ce nouveau service sera différente.

  1. Dans votre classe de test, créez une variable lateinit pour MarsApiService.
private lateinit var service: MarsApiService

Nous avons maintenant besoin d'une fonction qui définit la variable service avant chaque test.

  1. Créez une fonction appelée setup() et annotez-la avec @Before.
@Before
fun setup() {}

Pour ce test, nous n'allons pas utiliser d'URL pour nos requêtes réseau. C'est pour cela que MockWebServer s'avère pratique.

Simulation de données

Dans la classe BaseTest, il existe une propriété appelée mockWebServer,, qui est simplement une instance de l'objet MockWebServer. Cet objet va intercepter nos requêtes réseau, mais nous devons d'abord les diriger vers l'URL qui sera interceptée.

MockWebServer comporte une fonction appelée url() qui spécifie l'URL à intercepter. Souvenez-vous que nous ne voulons pas effectuer de véritable requête réseau. Nous voulons seulement faire semblant d'en créer une afin de pouvoir tester le code réseau avec les données que nous contrôlons dans le test lui-même. La fonction url() utilise une chaîne qui représente cette URL fictive et renvoie un objet HttpUrl. Dans votre fonction setup(), écrivez la ligne suivante :

val url = mockWebServer.url("/")

Nous avons défini le point de terminaison de l'URL que nous voulons intercepter, et nous avons capturé l'objet HttpUrl renvoyé.

Dans cette fonction, créez une instance de MarsApiService de la même manière que dans MarsApiService et MarsApiClass (à l'exception de la variable lazy). Toutefois, n'incluez pas d'URL de base pour le moment. Le résultat devrait ressembler à l'exemple suivant :

service = Retrofit.Builder()
   .addConverterFactory(MoshiConverterFactory.create(
       Moshi.Builder()
           .add(KotlinJsonAdapterFactory())
           .build()
   ))
   .build()
   .create(MarsApiService::class.java)

Vous pouvez maintenant définir l'URL de base. Ajoutez le code suivant à la chaîne de fonction de Retrofit.Builder() :

.baseUrl(url)

Ce code indique à notre service d'API que nous souhaitons acheminer les requêtes vers notre serveur fictif, MockWebServer.

service = Retrofit.Builder()
   .baseUrl(url)
   .addConverterFactory(MoshiConverterFactory.create(
       Moshi.Builder()
           .add(KotlinJsonAdapterFactory())
           .build()
   ))
   .build()
   .create(MarsApiService::class.java)

Là encore, le but du serveur fictif MockWebServer est d'éviter d'envoyer une véritable requête réseau à une API réelle. L'objectif est d'éviter que le test échoue en cas de défaillance de l'API, ce qui se produirait avec une requête réseau réelle. En utilisant une véritable API, nous testerions l'API elle-même, or notre but est de tester le code de notre projet Android.

Vous pouvez considérer MockWebServer comme une API fictive qui renvoie les données que nous avons créées. Par conséquent, nous devons explicitement indiquer à MockWebServer ce qu'il faut renvoyer avant qu'une requête soit effectuée. C'est là que la fonction enqueue() de BaseTest entre en jeu. Ne vous souciez pas trop du code de cette fonction, car nous n'allons pas nous y attarder dans cet atelier de programmation. Sachez simplement qu'elle prend un fichier de nos ressources de test et le transforme en une fausse réponse d'API.

  1. Créez une fonction de test appelée api_service(). Dans votre fonction de test, appelez la méthode enqueue comme suit :
enqueue("mars_photos.json")
  1. Nous allons ensuite appeler directement la fonction getPhotos() à partir de MarsApiService. N'oubliez pas que getPhotos() est une fonction de suspension et qu'elle doit être appelée à partir d'une portée de coroutine. Pour ce faire, encapsulons notre appel de la méthode dans runBlocking comme suit :
runBlocking {
    val apiResponse = service.getPhotos()
}
  1. Vérifions maintenant que la réponse getPhotos() n'est pas null. N'oubliez pas que nous avons défini apiResponse dans runBlocking. Nous devons donc y accéder également dans runBlocking.
runBlocking {
    val apiResponse = service.getPhotos()
    assertNotNull(apiResponse)
}

getPhotos() renvoie une liste contenant des objets MarsPhoto. Assurons-nous que la taille de la liste correspond à ce qui est attendu.

  1. Consultez le fichier mars_photos.json dans test/res. Créez une autre assertion pour vous assurer que la liste n'est pas vide. Créons également une assertion pour vérifier que certaines données sont correctes. Accédez à test/res/mars_photos.json et copiez l'un des ID. Déclarez que la valeur de cet ID est égale à celle de l'élément correspondant dans la liste.
runBlocking {
   val apiResponse = service.getPhotos()

   assertNotNull(apiResponse)
   assertTrue("The list was empty", apiResponse.isNotEmpty())
   assertEquals("The IDs did not match", "424905", apiResponse[0].id)
}

Le code de test devrait maintenant ressembler à ceci : 4000883c0d6d47bb.png

8. Code de solution

9. Félicitations !

Tester des requêtes réseau peut s'avérer particulièrement complexe, et cet atelier de programmation ne fait que survoler le sujet. Chaque test réseau que vous écrivez sera propre aux API à partir desquelles votre application reçoit des données. À mesure que vous apprenez à utiliser les API, vous pouvez développer vos tests pour simuler des défaillances du réseau et différentes réponses d'API. C'est un type de tests difficile à maîtriser, et il faut y aller à tâtons pour devenir un expert, donc continuez de vous entraîner.

Dans cet atelier de programmation, nous avons appris à :

  • appliquer la programmation orientée objet aux tests ;
  • stocker des ressources dans un répertoire de test ;
  • appeler des fonctions de suspension dans un test ;
  • simuler des réponses d'API lors d'un test.