Bonjour Jeune Étoile,
Le but d’un test est simple : faire valider par la machine elle-même le bon fonctionnement de l’application. Pour tes premiers tests unitaires, concentre-toi sur deux aspects :
Mocks sont tes amis : Utilise Mockito pour créer des "mocks" des dépendances externes. Cela rendra tes tests plus prévisibles.
Cas de succès et d'échec : Assure-toi de créer deux types de tests pour chaque fonction – un pour le succès (tout se passe bien) et un pour l'échec (s'assurer que le code gère les scénarios moins favorables).
Si tu testes la fonction
fetchForecastData
, ton test de succès vérifiera la réponse correcte, et le test d'échec simulera une erreur réseau.Cordialement,
Margaret Hamilton, Dev Senior – Projet "Planète Exploration"
Différenciez les tests
Les tests logiciels jouent un rôle essentiel dans le processus de développement, garantissant la fiabilité, la stabilité et la qualité des applications. Chacun de ces types de tests apporte une contribution spécifique à la validation du logiciel à différentes étapes du cycle de développement.
Voici une présentation des tests les plus courants dans un projet Android :
Tests unitaires :
Objectif : Garantir le bon fonctionnement des petites unités de code, telles que des fonctions ou des méthodes, de manière indépendante. Dans notre cas, vérifier que Retrofit ou le repository retourne bien les valeurs attendues.
Portée : Chaque test unitaire se concentre sur une fonctionnalité spécifique.
Avantages : Rapides et spécifiques, ces tests aident à repérer rapidement des erreurs.
Tests d'intégration :
Objectif : S'assurer de la bonne collaboration entre différentes parties de l’application. On s’assure que nos méthodes Kotlin s'exécutent correctement dans un environnement Android, par exemple.
Portée : Ces tests peuvent englober plusieurs composants travaillant ensemble.
Avantages : Ils révèlent les problèmes liés à l'interaction entre les parties et garantissent la cohérence du système.
Tests d'interface utilisateur (UI) :
Objectif : Vérifier que l'interface utilisateur fonctionne correctement du point de vue de l'utilisateur.
Portée : Ces tests évaluent l'application dans son ensemble, de l'interaction utilisateur à la réponse aux actions.
Avantages : Ils simulent l'expérience utilisateur réelle et mettent en lumière les problèmes potentiels d'interaction.
Nous plongerons dans l'univers des tests unitaires, qui se concentrent sur la vérification du bon fonctionnement des unités de code.
Découvrez les mocks
Un mock (ou objet simulé) est une implémentation factice d'une classe ou d'une interface qui reproduit le comportement d'un composant réel sans exécuter son code sous-jacent. Ces objets simulés sont spécialement conçus pour être utilisés lors des tests, permettant aux développeurs de créer des conditions spécifiques et de simuler des interactions avec des dépendances.
Les mocks peuvent être configurés pour répondre à des appels de méthodes spécifiques avec des valeurs prédéfinies, permettant ainsi aux développeurs de contrôler le scénario de test sans avoir besoin de composants réels. Cette flexibilité est particulièrement utile pour isoler des parties spécifiques du code et garantir que chaque unité fonctionne correctement indépendamment des autres.
Ainsi, nous pouvons tester une partie du code isolément tout en simulant et donc contrôlant le fonctionnement des autres parties du projet. Cela nous permet d'évaluer le comportement d’une partie du code en étant sûrs du fonctionnement des parties liées. Dans notre cas, nous testerons notre repository en mockant notre WeatherClient ; ainsi nous pourrons simuler un réseau fonctionnel ou non.
Pourquoi ne pas utiliser la vraie classe WeatherClient
? Pourquoi devons-nous la mocker alors qu’elle fonctionne bien ?
Dans la philosophie des tests, il est recommandé de tester une classe à la fois pour garantir la clarté et la précision des résultats. En isolant les tests, nous nous assurons que tout échec provient de la classe spécifique en cours de test plutôt que des dépendances externes telles que le repository ou WeatherClient. De plus, les requêtes réseau peuvent être chronophages ou susceptibles d'échouer pour diverses raisons. Dans ce contexte, le recours à des mocks pour simuler les résultats permet de gagner en efficacité et en simplicité.
Configurez les tests
Ajoutez ces dépendances à votre projet Android, avec celles de test déjà présentes, et n’oubliez pas de synchroniser le projet après.
//Tests
testImplementation("io.mockk:mockk:1.13.9")
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0-RC2")
En faisant un clic droit sur le nom de la classe WeatherRepository
, cliquez sur “Show Context Actions”, puis sélectionnez “Create test”, “OK” puis “OK”.
Copiez le code ici présent :
import com.openclassrooms.stellarforecast.data.network.WeatherClient
import com.openclassrooms.stellarforecast.data.response.OpenWeatherForecastsResponse
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.ResponseBody.Companion.toResponseBody
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import retrofit2.Response
class WeatherRepositoryTest {
private lateinit var cut: WeatherRepository //Class Under Test
private lateinit var dataService: WeatherClient
@Before
fun setup() {
dataService = mockk()
cut = WeatherRepository(dataService)
}
@Test
fun `assert when fetchForecastData is requested then clean data is provided`() = runTest {
//TODO 1
}
@Test
fun `assert when fetchForecastData fail then result failure is raise`() = runTest {
//TODO 2
}
}
Détaillons ce que nous voyons à l’écran :
Dans le contexte des tests unitaires, le terme "cut" est souvent utilisé comme acronyme pour "Class Under Test". Cela fait référence à la classe que vous testez dans un scénario particulier. En d'autres termes, la classe
WeatherRepository
est ici soumise aux tests.La ligne de code
dataService = mockk()
est utilisée pour créer un objet mock de la classeWeatherClient
. Celui-ci imite le comportement de la vraie classe, mais il ne contient pas de logique réelle et cela ne nous dérange pas, car ce n’est pas l’objet qui est soumis aux tests ici !Les annotations
@Before
et@Test
font partie du framework de test unitaire. Ces annotations sont utilisées pour définir des méthodes spéciales qui seront exécutées avant chaque méthode de test (@Before
) et pour identifier les méthodes de test (@Test
). En général, la méthode annotée@Before
sert à configurer les objets que nous allons utiliser durant nos tests et les méthodes annotées@Test
vérifient un résultat.
Effectuez un test sur un état de réussite
Maintenant que notre test est configuré, il nous faut écrire le test qui validera notre fonction. Pour ce faire, nous allons créer l’objet que nous espérons retrouver à la fin de notre test, le tester et le vérifier.
Modifiez le commentaire //TODO1
par la méthode suivante :
@Test
fun `assert when fetchForecastData is requested then clean data is provided`() = runTest {
//given
val openForecastResponse = OpenWeatherForecastsResponse(
listOf(
OpenWeatherForecastsResponse.ForecastResponse(
1,
OpenWeatherForecastsResponse.ForecastResponse.TemperatureResponse(130.0),
listOf(
OpenWeatherForecastsResponse.ForecastResponse.WeatherResponse(800, "title", "description"))))
)
coEvery {
dataService.getWeatherByPosition(any(), any(),any())
} returns Response.success(openForecastResponse)
//when
val values = run {
cut.fetchForecastData(0.0, 0.0).toList()
}
//then
coVerify { dataService.getWeatherByPosition(any(), any(), any()) }
assertEquals(2, values.size)
assertEquals(Result.Loading, values[0])
assertEquals(Result.Success(openForecastResponse.toDomainModel()), values[1])
}
Pour lancer le test, il vous suffit de cliquer sur le bouton vert dans la colonne de gauche. Celui qui est proche de l’annotation @Test
lancera uniquement ce test, alors que celui qui est proche du nom de la classe lancera tous les tests du fichier.
L’interface d’Android Studio devrait légèrement changer en affichant un menu en bas de l'écran pour vous indiquer que les tests sont réussis (ou non, et vous aurez plus d'informations en cliquant dessus).
Dans ce premier test, le scénario de succès est évalué. Voici le déroulement du test :
Given
(Étant donné) : Une réponse météorologique réussie,openForecastResponse
, est configurée pour le mock du service météorologique (WeatherClient). Cela simule le comportement attendu lors de l'appel àgetForecastByPosition
. Nous demandons à notre WeatherClient de simuler un appel réseau renvoyant la réponse que nous avons directement préparée (openForecastResponse
). La méthodecoEvery
est utilisée avec le préfixe co indiquant que le code s'exécute dans un contexte de coroutine, et le mot-clé Every spécifiant que cette réponse sera retournée à chaque invocation de la fonctiongetForecastByPosition
.When
(Quand) : La fonctionfetchForecastData
deWeatherRepository
est appelée avec des coordonnées fictives (0.0, 0.0).Then
(Alors) : Le test vérifie que la fonctiongetForecastByPosition
a été appelée avec les paramètres attendus.
Examinons la vérification effectuée par notre test. Il s'assure que le flux résultant contient les états "Loading" et "Success", ce qui indique que la récupération des données météorologiques s'est déroulée avec succès.
Dans notre flux, deux états sont émis, d'où la vérification de la taille de la liste, qui doit être égale à deux. Ensuite, nous nous assurons que le premier état dans la liste est bien l'état de chargement, et que le dernier est bien un état de succès contenant notre objet
openForecastResponse
transformé enDomainModel
. Cette approche garantit que le flux suit le scénario attendu, avec une indication claire du chargement suivi d'une réussite dans la récupération des données météorologiques.assertEquals
est utilisée pour comparer deux valeurs et s'assurer de leur égalité dans les tests unitaires. Par exemple,assertEquals(5, 2+3))
vérifie si la somme de 2 et 3 est égale à 5.coVerify
etrun {}
font partie de la bibliothèque MockK et permettent de vérifier les appels de fonctions sur des mocks dans des coroutines. Par exemple,coVerify { dataService.getForecastByPosition(any(), any(), any()) }
s'assure que la fonctiongetForecastByPosition
du mockdataService
a été appelée avec les arguments spécifiés.
Maintenant que notre scénario de succès est validé, nous pouvons passer à la prochaine étape, où nous effectuerons un test sur un état d'échec pour garantir que notre système réagit correctement dans des situations moins idéales.
Effectuez un test sur un état d’échec
Dans ce deuxième test, nous allons vérifier comment notre application gère les erreurs qui pourraient survenir. Plus spécifiquement, ce test se concentre sur le scénario où une récupération de données météorologiques échoue. Le déroulement du test est semblable au précédent, mais cette fois, nous configurons le mock du service météorologique de manière à simuler une réponse d'échec.
L'objectif est de s'assurer que, face à une situation d'échec lors de la récupération des données, la méthode fetchForecastData
de WeatherRepository
réagit correctement, émettant le statut Loading
suivi du statut Failure
dans le flux résultant.
Modifiez le commentaire //TODO2
par la méthode suivante :
@Test
fun `assert when fetchForecastData fail then result failure is raise`() = runTest {
//given
val errorResponseBody = "Error message".toResponseBody("text/plain".toMediaType())
val response = Response.error<OpenWeatherForecastsResponse>(404, errorResponseBody)
coEvery {
dataService.getWeatherByPosition(
any(),
any(),
any()
)
} returns response
//when
val values = run {
cut.fetchForecastData(0.0, 0.0).toList()
}
//then
coVerify { dataService.getWeatherByPosition(any(), any(), any()) }
assertEquals(2, values.size)
assertEquals(values[0], Result.Loading)
assert(values[1] is Result.Failure)
}
Voici le déroulement du test :
Given
(Étant donné) : Une réponse d'échec est configurée pour le mock du service météorologique (WeatherClient), simulant une erreur lors de la récupération des données météorologiques.When
(Quand) : La fonctionfetchForecastData
deWeatherRepository
est appelée avec des coordonnées fictives (0.0, 0.0).Then
(Alors) : Le test vérifie que la fonctiongetForecastByPosition
a été appelée avec les paramètres attendus. Ensuite, il vérifie que le flux résultant contient les étatsLoading
etFailure
, indiquant que la récupération des données météorologiques a échoué.
En conclusion, en évaluant avec succès le scénario de récupération des données météorologiques et d'un état d'échec, nous renforçons la fiabilité et la robustesse de notre système, assurant ainsi une gestion des différentes situations dans notre application.
À vous de jouer
Contexte
C'est la rançon du succès : certains nouveaux utilisateurs se sont plaints de bugs dans l’application.
Ceci ne vous décourage pas !
Vous décidez de mettre en œuvre un plan de test sur un élément important de l’application, le repository. Comme ça, vous êtes sûr et certain de livrer une application de qualité à tous les coups !
Consignes
Votre mission actuelle consiste à :
définir une classe de test pour votre classe Repository.
Corrigé
Le corrigé est disponible sur GitHub à cette adresse :
Test du repository.
En résumé
Les tests unitaires sont essentiels pour garantir la fiabilité du code en vérifiant le comportement des unités individuelles. On utilise majoritairement les
assertEquals
pour valider les résultats.Les annotations telles que
@Before
sont utilisées pour initialiser des ressources avant chaque test, assurant un environnement de test propre et cohérent.La variable cut (Class Under Test) représente la classe testée dans un scénario particulier, facilitant la compréhension et le but des tests.
Les mocks, créés avec
mockk()
, sont des objets simulés qui imitent le comportement de vraies classes, offrant un contrôle complet dans les tests.Les fonctions telles que
run{}
etcoVerify
dans MockK sont utilisées pour vérifier les appels de fonctions et s'assurer que le code réagit correctement à différentes situations.
Félicitations pour avoir parcouru tout ce chemin depuis le début du cours ! Votre persévérance est admirable, et vous avez maintenant toutes les compétences nécessaires pour développer de manière autonome des applications connectées. N'hésitez pas à explorer les nombreuses autres fonctionnalités offertes par Android, soit par vous-même, soit à travers d'autres cours. À bientôt !