1. Avant de commencer
Cet atelier de programmation vous explique comment écrire des tests unitaires pour tester le composant ViewModel
. Vous allez ajouter des tests unitaires pour l'application de jeu Unscramble. Cette application est un jeu de lettres amusant qui consiste à deviner un mot mélangé et à gagner des points lorsque vous le devinez. L'image suivante présente un aperçu de l'application :
Dans l'atelier de programmation Écrire des tests automatisés, vous avez découvert les tests automatisés et leur importance. Vous avez également découvert comment implémenter des tests unitaires.
Les points suivants ne devraient maintenant plus avoir de secrets pour vous :
- Les tests automatisés sont du code qui permet de vérifier l'exactitude d'un autre élément de code.
- Les tests constituent une partie importante du processus de développement d'applications. En effectuant régulièrement des tests sur votre application, vous pouvez vérifier son comportement et son utilisation avant de la mettre à disposition de tous.
- Les tests unitaires vous permettent de tester des fonctions, des classes et des propriétés.
- Les tests unitaires locaux sont exécutés sur votre poste de travail, ce qui signifie qu'ils s'exécutent dans un environnement de développement sans nécessiter d'appareil Android ni d'émulateur. En d'autres termes, les tests en local sont exécutés sur votre ordinateur.
Avant de continuer, veillez à suivre les ateliers de programmation Écrire des tests automatisés et ViewModel et état dans Compose.
Conditions préalables
- Vous maîtrisez Kotlin, y compris les fonctions, les lambdas et les composables sans état.
- Vous disposez de connaissances de base en création de mises en page dans Jetpack Compose.
- Vous disposez de connaissances de base en Material Design.
- Vous disposez de connaissances de base sur la mise en œuvre de ViewModel.
Points abordés
- Ajouter des dépendances aux tests unitaires dans le fichier
build.gradle.kts
du module d'application - Créer une stratégie de test pour implémenter des tests unitaires
- Écrire des tests unitaires à l'aide de JUnit4 et comprendre le cycle de vie des instances de test
- Exécuter, analyser et améliorer la couverture du code
Objectifs de l'atelier
- Tests unitaires pour l'application de jeu Unscramble
Ce dont vous avez besoin
- La dernière version d'Android Studio
Télécharger le code de démarrage
Pour commencer, téléchargez le code de démarrage :
Vous pouvez également cloner le dépôt GitHub du code :
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout viewmodel
Vous pouvez parcourir le code dans le dépôt GitHub Unscramble
.
2. Présentation du code de démarrage
Dans le module 2, vous avez appris à placer le code du test unitaire dans l'ensemble de sources test qui se trouve sous le dossier src, comme illustré ci-dessous :
Le code de démarrage contient le fichier suivant :
WordsData.kt
: ce fichier contient la liste des mots à utiliser pour le test, ainsi qu'une fonction d'assistancegetUnscrambledWord()
permettant de générer le mot réel à partir du mot mélangé. Vous n'avez pas besoin de modifier ce fichier.
3. Ajouter des dépendances de test
Dans cet atelier de programmation, vous utiliserez le framework JUnit pour écrire des tests unitaires. Pour utiliser ce framework, vous devez l'ajouter en tant que dépendance dans le fichier build.gradle.kts
de votre module d'application.
La configuration implementation
vous permet de spécifier les dépendances requises par votre application. Par exemple, pour utiliser la bibliothèque ViewModel
dans l'application, vous devez ajouter une dépendance à androidx.lifecycle:lifecycle-viewmodel-compose
, comme indiqué dans l'extrait de code suivant :
dependencies {
...
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}
Vous pouvez désormais utiliser cette bibliothèque dans le code source de votre application. Android Studio l'ajoutera alors au fichier APK (Application Package File) généré. Toutefois, il est déconseillé d'ajouter le code de test unitaire dans ce fichier APK. Le code de test n'ajoute aucune fonctionnalité utile et aurait également un impact sur la taille du fichier APK. Il en va de même pour les dépendances requises par le code de test. Vous devez les isoler. Pour ce faire, utilisez la configuration testImplementation
, qui indique qu'elle s'applique au code source du test local et non au code de l'application.
Pour ajouter une dépendance à votre projet, spécifiez une configuration de dépendance (comme implementation
ou testImplementation
) dans le bloc de dépendances de votre fichier build.gradle.kts
. Chaque configuration de dépendance fournit à Gradle des instructions différentes sur l'utilisation de cette dépendance.
Pour ajouter une dépendance, procédez comme suit :
- Ouvrez le fichier
build.gradle.kts
du moduleapp
, situé dans le répertoireapp
du volet Project (Projet).
- Faites défiler le fichier vers le bas jusqu'au bloc
dependencies{}
. Ajoutez une dépendance pourjunit
à l'aide de la configurationtestImplementation
.
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("junit:junit:4.13.2")
}
- Dans la barre de notification en haut du fichier build.gradle.kts, cliquez sur Sync Now (Synchroniser) pour terminer l'importation et la compilation, comme illustré dans la capture d'écran suivante :
Nomenclature Compose
La nomenclature Compose est la méthode recommandée pour gérer les versions de la bibliothèque Compose. Elle vous permet de gérer toutes les versions de votre bibliothèque Compose en ne spécifiant que la version de la nomenclature.
Notez la section des dépendances dans le fichier build.gradle.kts
du module app
.
// No need to copy over
// This is part of starter code
dependencies {
// Import the Compose BOM
implementation (platform("androidx.compose:compose-bom:2023.06.01"))
...
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
...
}
Tenez compte des points suivants :
- Les numéros de version de la bibliothèque Compose ne sont pas spécifiés.
- La nomenclature est importée avec
implementation platform("androidx.compose:compose-bom:2023.06.01")
.
Cela s'explique par le fait que la nomenclature elle-même contient des liens vers les dernières versions stables des différentes bibliothèques Compose, de manière à ce qu'elles fonctionnent correctement ensemble. Lorsque vous utilisez la nomenclature dans votre application, vous n'avez pas besoin d'ajouter de versions aux dépendances des bibliothèques Compose elles-mêmes. Lorsque vous mettez à jour la version de la nomenclature, toutes les bibliothèques que vous utilisez sont automatiquement mises à jour vers leur nouvelle version.
Pour utiliser la nomenclature avec les bibliothèques de test Compose (tests instrumentés), vous devez importer androidTestImplementation platform("androidx.compose:compose-bom:xxxx.xx.xx")
. Vous pouvez créer une variable et la réutiliser pour implementation
et androidTestImplementation
, comme indiqué.
// Example, not need to copy over
dependencies {
// Import the Compose BOM
implementation(platform("androidx.compose:compose-bom:2023.06.01"))
implementation("androidx.compose.material:material")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
// ...
androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
Félicitations ! Vous avez ajouté des dépendances de test à l'application. Vous êtes maintenant prêt à ajouter des tests unitaires.
4. Stratégie de test
Une bonne stratégie de test consiste à couvrir différents chemins et différentes limites de votre code. À un niveau très basique, vous pouvez classer les tests dans trois scénarios : réussite, erreur et cas limite.
- Réussite : les tests de réussite visent à évaluer le fonctionnement d'un flux positif. Un flux positif est un flux sans conditions d'exception ou d'erreur. Par rapport aux scénarios d'erreur et de cas limite, il est facile de créer une liste exhaustive de scénarios de réussite, car ils se concentrent sur le comportement souhaité de l'application.
Un exemple de réussite dans l'application Unscramble est la mise à jour correcte du score, du nombre de mots et du mot mélangé lorsque l'utilisateur saisit un mot correct et clique sur le bouton Submit (Envoyer).
- Erreur : les tests d'erreur visent à tester la fonctionnalité d'un flux négatif. Autrement dit, ils vérifient la réponse de l'application à des conditions d'erreur ou à des entrées utilisateur non valides. Il est assez difficile de déterminer tous les flux d'erreurs, car il existe de nombreux résultats possibles lorsque le comportement attendu n'est pas atteint.
Un conseil d'ordre général consiste à lister tous les chemins d'erreur possibles, à les écrire et à faire évoluer vos tests unitaires à mesure que vous découvrez différents scénarios.
Par exemple, dans l'application Unscramble, l'utilisateur a saisi un mot incorrect et a cliqué sur le bouton Envoyer. Par conséquent, un message d'erreur s'affiche, et le score et le nombre de mots ne sont pas mis à jour.
- Cas limite : un cas limite vise à tester les conditions limites de l'application. Dans l'application Unscramble, une limite vérifie l'état de l'interface utilisateur lors du chargement de l'application, et l'état de l'interface utilisateur après que l'utilisateur a lu un nombre maximal de mots.
La création de scénarios de test pour ces catégories pourra vous servir de guide pour votre stratégie de test.
Créer des tests
Un test unitaire efficace remplit généralement quatre critères :
- Il est ciblé : l'objectif est de tester une unité, comme un extrait de code. Il s'agit souvent d'une classe ou d'une méthode. Le test doit être concis et se concentrer sur la validation de l'exactitude de chaque élément de code, plutôt que sur l'exécution simultanée de plusieurs éléments de code.
- Il est compréhensible : le code doit être simple et facile à comprendre. En bref, le développeur doit pouvoir interpréter immédiatement l'intention qui se cache derrière le test.
- Il est déterministe : il doit toujours réussir ou échouer. Lorsque vous exécutez les tests autant de fois que vous le souhaitez, sans modifier le code, vous devez obtenir le même résultat. Le test ne doit pas être irrégulier, avec un échec dans une instance et une réussite dans une autre instance malgré l'absence de modification du code.
- Il est autonome : il ne nécessite aucune configuration ou interaction humaine et fonctionne de manière indépendante.
Scénario de réussite
Pour écrire un test unitaire pour un scénario de réussite, vous devez confirmer qu'une instance de GameViewModel
a été initialisée lorsque la méthode updateUserGuess()
est appelée avec la bonne réponse, suivie d'un appel à checkUserGuess()
, puis des actions suivantes :
- La bonne réponse est transmise à la méthode
updateUserGuess()
. - La méthode
checkUserGuess()
est appelée. - La valeur des états
score
etisGuessedWordWrong
est mise à jour correctement.
Pour créer le test, procédez comme suit :
- Créez un package
com.example.android.unscramble.ui.test
sous l'ensemble de sources de test, puis ajoutez le fichier comme illustré dans la capture d'écran suivante :
Pour écrire un test unitaire pour la classe GameViewModel
, vous avez besoin d'une instance de cette classe qui vous permet d'appeler ses méthodes et de vérifier son état.
- Dans le corps de la classe
GameViewModelTest
, déclarez une propriétéviewModel
et attribuez-lui une instance de la classeGameViewModel
.
class GameViewModelTest {
private val viewModel = GameViewModel()
}
- Pour écrire un test unitaire pour le scénario de réussite, créez une fonction
gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset()
et marquez-la avec l'annotation@Test
.
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
}
}
- Exécutez la commande d'importation suivante :
import org.junit.Test
Pour transmettre un mot correct du joueur à la méthode viewModel.updateUserGuess()
, vous devez générer le mot réel correct à partir du mot mélangé dans GameUiState
. Pour ce faire, commencez par obtenir l'état actuel de l'interface de jeu.
- Dans le corps de la fonction, créez une variable
currentGameUiState
et attribuez-luiviewModel.uiState.value
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
}
- Pour obtenir le mot correct deviné par le joueur, utilisez la fonction
getUnscrambledWord()
, qui prendcurrentGameUiState.currentScrambledWord
comme argument et renvoie le mot réel. Stockez cette valeur renvoyée dans une nouvelle variable en lecture seule nomméecorrectPlayerWord
et attribuez la valeur renvoyée par la fonctiongetUnscrambledWord()
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Pour vérifier si le mot deviné est correct, ajoutez un appel à la méthode
viewModel.updateUserGuess()
et transmettez la variablecorrectPlayerWord
en tant qu'argument. Ajoutez ensuite un appel à la méthodeviewModel.checkUserGuess()
pour vérifier le mot deviné.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
}
Vous êtes maintenant prêt à vérifier que l'état du jeu correspond à vos attentes.
- Récupérez l'instance de la classe
GameUiState
à partir de la valeur de la propriétéviewModel.uiState
, puis stockez-la dans la variablecurrentGameUiState
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
}
- Pour vous assurer que le mot deviné est correct et que le score est mis à jour, utilisez la fonction
assertFalse()
pour vérifier que la propriétécurrentGameUiState.isGuessedWordWrong
estfalse
, et la fonctionassertEquals()
pour vérifier que la valeur de la propriétécurrentGameUiState.score
est égale à20
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
// Assert that checkUserGuess() method updates isGuessedWordWrong is updated correctly.
assertFalse(currentGameUiState.isGuessedWordWrong)
// Assert that score is updated correctly.
assertEquals(20, currentGameUiState.score)
}
- Exécutez la commande d'importation suivante :
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
- Pour rendre la valeur
20
lisible et réutilisable, créez un objet associé et attribuez20
à une constanteprivate
nomméeSCORE_AFTER_FIRST_CORRECT_ANSWER
. Mettez à jour le test avec la constante qui vient d'être créée.
class GameViewModelTest {
...
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
...
// Assert that score is updated correctly.
assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
}
companion object {
private const val SCORE_AFTER_FIRST_CORRECT_ANSWER = SCORE_INCREASE
}
}
- Exécutez le test.
Il devrait réussir, car toutes les assertions sont valides, comme illustré dans la capture d'écran suivante :
Scénario d'erreur
Pour écrire un test unitaire pour un scénario d'erreur, vous devez confirmer que lorsqu'un mot incorrect est transmis en tant qu'argument à la méthode viewModel.updateUserGuess()
et que la méthode viewModel.checkUserGuess()
est appelée, voici ce qui se produit :
- La valeur de la propriété
currentGameUiState.score
reste la même. - La valeur de la propriété
currentGameUiState.isGuessedWordWrong
est définie surtrue
, car le mot deviné est incorrect.
Pour créer le test, procédez comme suit :
- Dans le corps de la classe
GameViewModelTest
, créez une fonctiongameViewModel_IncorrectGuess_ErrorFlagSet()
et marquez-la avec l'annotation@Test
.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
}
- Définissez une variable
incorrectPlayerWord
et attribuez-lui la valeur"and"
, qui ne doit pas figurer dans la liste de mots.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
}
- Ajoutez un appel à la méthode
viewModel.updateUserGuess()
et transmettez la variableincorrectPlayerWord
en tant qu'argument. - Ajoutez un appel à la méthode
viewModel.checkUserGuess()
pour vérifier le mot deviné.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
}
- Ajoutez une variable
currentGameUiState
et attribuez-lui la valeur de l'étatviewModel.uiState.value
. - Utilisez des fonctions d'assertion pour confirmer que la valeur de la propriété
currentGameUiState.score
est0
et que la valeur de la propriétécurrentGameUiState.isGuessedWordWrong
est définie surtrue
.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
val currentGameUiState = viewModel.uiState.value
// Assert that score is unchanged
assertEquals(0, currentGameUiState.score)
// Assert that checkUserGuess() method updates isGuessedWordWrong correctly
assertTrue(currentGameUiState.isGuessedWordWrong)
}
- Exécutez la commande d'importation suivante :
import org.junit.Assert.assertTrue
- Exécutez le test pour vérifier qu'il a réussi.
Scénario de limite
Pour tester l'état initial de l'interface utilisateur, vous devez écrire un test unitaire pour la classe GameViewModel
. Ce test doit confirmer que lorsque GameViewModel
est initialisé, les conditions suivantes sont remplies :
- La propriété
currentWordCount
est définie sur1
. - La propriété
score
est définie sur0
. - La propriété
isGuessedWordWrong
est définie surfalse
. - La propriété
isGameOver
est définie surfalse
.
Pour ajouter le test, procédez comme suit :
- Créez une méthode
gameViewModel_Initialization_FirstWordLoaded()
et marquez-la avec l'annotation@Test
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
}
- Accédez à la propriété
viewModel.uiState.value
pour obtenir l'instance initiale de la classeGameUiState
. Attribuez-la à une nouvelle variablegameUiState
en lecture seule.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
}
- Pour obtenir le mot correct deviné par le joueur, utilisez la fonction
getUnscrambledWord()
, qui récupère le motgameUiState.currentScrambledWord
et renvoie le mot réel. Attribuez la valeur renvoyée à une nouvelle variable en lecture seule nomméeunScrambledWord
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
}
- Pour vérifier que l'état est correct, ajoutez les fonctions
assertTrue()
afin de confirmer que la propriétécurrentWordCount
est définie sur1
et que la propriétéscore
est définie sur0
. - Ajoutez des fonctions
assertFalse()
pour vérifier que la propriétéisGuessedWordWrong
estfalse
et que la propriétéisGameOver
est définie surfalse
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
// Assert that current word is scrambled.
assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
// Assert that current word count is set to 1.
assertTrue(gameUiState.currentWordCount == 1)
// Assert that initially the score is 0.
assertTrue(gameUiState.score == 0)
// Assert that the wrong word guessed is false.
assertFalse(gameUiState.isGuessedWordWrong)
// Assert that game is not over.
assertFalse(gameUiState.isGameOver)
}
- Exécutez la commande d'importation suivante :
import org.junit.Assert.assertNotEquals
- Exécutez le test pour vérifier qu'il a réussi.
Un autre scénario de limite consiste à tester l'état de l'interface utilisateur une fois que l'utilisateur a deviné tous les mots. Vous devez confirmer que, lorsque l'utilisateur devine tous les mots correctement, les conditions suivantes s'appliquent :
- Le score est à jour.
- La propriété
currentGameUiState.currentWordCount
est égale à la valeur de la constanteMAX_NO_OF_WORDS
. - La propriété
currentGameUiState.isGameOver
est définie surtrue
.
Pour ajouter le test, procédez comme suit :
- Créez une méthode
gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly()
et marquez-la avec l'annotation@Test
. Dans la méthode, créez une variableexpectedScore
et attribuez-lui0
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
}
- Pour obtenir l'état initial, ajoutez une variable
currentGameUiState
et attribuez-lui la valeur de la propriétéviewModel.uiState.value
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
}
- Pour obtenir le mot correct deviné par le joueur, utilisez la fonction
getUnscrambledWord()
, qui récupère le motcurrentGameUiState.currentScrambledWord
et renvoie le mot réel. Stockez cette valeur renvoyée dans une nouvelle variable en lecture seule nomméecorrectPlayerWord
et attribuez la valeur renvoyée par la fonctiongetUnscrambledWord()
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Pour vérifier si l'utilisateur devine toutes les réponses, utilisez un bloc
repeat
afin de répéter l'exécution de la méthodeviewModel.updateUserGuess()
et de la méthodeviewModel.checkUserGuess()
MAX_NO_OF_WORDS
fois.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
}
}
- Dans le bloc
repeat
, ajoutez la valeur de la constanteSCORE_INCREASE
à la variableexpectedScore
pour indiquer que le score augmente après chaque bonne réponse. - Ajoutez un appel à la méthode
viewModel.updateUserGuess()
et transmettez la variablecorrectPlayerWord
en tant qu'argument. - Ajoutez un appel à la méthode
viewModel.checkUserGuess()
pour déclencher la vérification du mot deviné par l'utilisateur.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
}
}
- Mettez à jour le mot actuel du joueur à l'aide de la fonction
getUnscrambledWord()
, qui récupèrecurrentGameUiState.currentScrambledWord
comme argument et renvoie le mot réel. Stockez cette valeur renvoyée dans une nouvelle variable en lecture seule nomméecorrectPlayerWord.
. Pour vérifier que l'état est correct, ajoutez la fonctionassertEquals()
afin de vérifier si la valeur de la propriétécurrentGameUiState.score
est égale à la valeur de la variableexpectedScore
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
// Assert that after each correct answer, score is updated correctly.
assertEquals(expectedScore, currentGameUiState.score)
}
}
- Ajoutez une fonction
assertEquals()
pour confirmer que la valeur de la propriétécurrentGameUiState.currentWordCount
est égale à la valeur de la constanteMAX_NO_OF_WORDS
et que la valeur de la propriétécurrentGameUiState.isGameOver
est définie surtrue
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
// Assert that after each correct answer, score is updated correctly.
assertEquals(expectedScore, currentGameUiState.score)
}
// Assert that after all questions are answered, the current word count is up-to-date.
assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
// Assert that after 10 questions are answered, the game is over.
assertTrue(currentGameUiState.isGameOver)
}
- Exécutez la commande d'importation suivante :
import com.example.unscramble.data.MAX_NO_OF_WORDS
- Exécutez le test pour vérifier qu'il a réussi.
Présentation du cycle de vie des instances de test
Si vous examinez attentivement la façon dont viewModel
s'initialise dans le test, vous remarquerez peut-être que viewModel
ne s'initialise qu'une seule fois, même si tous les tests l'utilisent. Cet extrait de code affiche la définition de la propriété viewModel
.
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
...
}
...
}
Vous vous posez peut-être les questions suivantes :
- Cela signifie-t-il que la même instance de
viewModel
est réutilisée pour tous les tests ? - Cela pose-t-il problème ? Par exemple, que se passe-t-il si la méthode de test
gameViewModel_Initialization_FirstWordLoaded
s'exécute après la méthode de testgameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset
? Le test d'initialisation échouera-t-il ?
La réponse à ces deux questions est non. Les méthodes de test sont exécutées séparément pour éviter les effets secondaires inattendus pouvant découler de l'état de l'instance de test modifiable. Par défaut, JUnit crée une instance de la classe de test avant l'exécution de chaque méthode de test.
Comme vous disposez de quatre méthodes de test jusqu'à présent dans votre classe GameViewModelTest
, GameViewModelTest
est instancié quatre fois. Chaque instance possède sa propre copie de la propriété viewModel
. Par conséquent, la séquence d'exécution du test n'a pas d'importance.
5. Présentation de la couverture de code
La couverture de code joue un rôle essentiel pour déterminer si vous testez correctement les classes, les méthodes et les lignes de code qui composent votre application.
Android Studio fournit un outil de couverture pour les tests unitaires locaux afin de suivre le pourcentage et les zones de votre code d'application couverts par vos tests unitaires.
Effectuer un test de couverture avec Android Studio
Pour exécuter des tests de la couverture, procédez comme suit :
- Effectuez un clic droit sur le fichier
GameViewModelTest.kt
dans le volet du projet, puis sélectionnez Run 'GameViewModelTest' with Coverage (Exécuter "GameViewModelTest" avec la couverture).
- Une fois l'exécution du test terminée, cliquez sur l'option Flatten Packages (Aplatir les packages) dans le volet de couverture à droite.
- Notez le package
com.example.android.unscramble.ui
, comme illustré dans l'image ci-dessous.
- Double-cliquez sur le nom du package
com.example.android.unscramble.ui
pour afficher la couverture deGameViewModel
, comme illustré dans l'image suivante :
Analyser le rapport de test
Le rapport illustré dans le schéma suivant est divisé en deux sections :
- Pourcentage de méthodes couvertes par les tests unitaires : dans l'exemple de diagramme, les tests que vous avez écrits ont couvert jusqu'à sept méthodes sur huit. Cela représente 87 % du total des méthodes.
- Pourcentage de lignes couvertes par les tests unitaires : dans cet exemple de diagramme, les tests que vous avez écrits ont couvert 39 lignes de code sur 41. Cela correspond à 95 % des lignes de code.
Ces rapports suggèrent que les tests unitaires que vous avez écrits jusqu'à présent ont manqué certaines parties du code. Pour identifier les parties manquantes, procédez comme suit :
- Double-cliquez sur GameViewModel.
Android Studio affiche le fichier GameViewModel.kt
avec un code couleur supplémentaire sur le côté gauche de la fenêtre. La couleur vert clair indique que ces lignes de code ont été couvertes.
En faisant défiler la page vers le bas dans GameViewModel
, vous remarquerez peut-être que quelques lignes sont marquées d'une couleur rose clair. Cette couleur indique que ces lignes de code n'ont pas été couvertes par les tests unitaires.
Améliorer la couverture
Pour améliorer la couverture, vous devez écrire un test qui couvre le scénario manquant. Vous devez ajouter un test pour confirmer que lorsqu'un utilisateur ignore un mot, les conditions suivantes s'appliquent :
- La propriété
currentGameUiState.score
reste la même. - La propriété
currentGameUiState.currentWordCount
est incrémentée d'une unité, comme indiqué dans l'extrait de code suivant.
Pour améliorer la couverture, ajoutez la méthode de test suivante à la classe GameViewModelTest
.
@Test
fun gameViewModel_WordSkipped_ScoreUnchangedAndWordCountIncreased() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
val lastWordCount = currentGameUiState.currentWordCount
viewModel.skipWord()
currentGameUiState = viewModel.uiState.value
// Assert that score remains unchanged after word is skipped.
assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
// Assert that word count is increased by 1 after word is skipped.
assertEquals(lastWordCount + 1, currentGameUiState.currentWordCount)
}
Pour réexécuter la couverture, procédez comme suit :
- Effectuez un clic droit sur le fichier
GameViewModelTest.kt
, puis sélectionnez Run 'GameViewModelTest' with Coverage (Exécuter "GameViewModelTest" avec la couverture) dans le menu. - Une fois la compilation terminée, accédez à nouveau à l'élément GameViewModel et vérifiez que le pourcentage de couverture est de 100 %. Le rapport final de couverture est illustré dans l'image ci-dessous.
- Accédez au fichier
GameViewModel.kt
et faites-le défiler vers le bas pour vérifier si le scénario que vous aviez manqué est désormais couvert.
Vous avez appris à exécuter, analyser et améliorer la couverture du code de votre application.
Un pourcentage élevé de couverture est-il un gage de qualité du code de l'application ? Non. La couverture de code indique le pourcentage de code couvert ou exécuté par le test unitaire. Cela ne signifie pas que le code a été validé. Si vous supprimez toutes les assertions du code de votre test unitaire et que vous exécutez la couverture de code, elle affiche toujours une couverture de 100 %.
Une couverture élevée n'indique pas que les tests sont conçus correctement ni qu'ils valident le comportement de l'application. Vous devez vous assurer que les tests que vous avez écrits comportent les assertions qui valideront le comportement de la classe testée. De plus, vous n'avez pas besoin d'écrire des tests unitaires pour obtenir une couverture de test de 100 % pour l'ensemble de l'application. Nous vous conseillons de tester certaines parties du code de l'application, comme les activités, à l'aide de tests de l'interface utilisateur.
Toutefois, une faible couverture signifie que de grandes parties de votre code n'ont pas été testées. Utilisez la couverture de code pour identifier les parties du code qui n'ont pas été exécutées par les tests, plutôt que pour évaluer la qualité du code.
6. Télécharger le code de solution
Pour télécharger le code de cet atelier de programmation terminé, utilisez les commandes Git suivantes :
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout main
Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.
Si vous souhaitez voir le code de solution, affichez-le sur GitHub.
7. Conclusion
Félicitations ! Vous avez appris à définir une stratégie de test et à implémenter des tests unitaires pour tester ViewModel
et StateFlow
dans l'application Unscramble. Lorsque vous continuerez à créer des applications Android, veillez à écrire les tests parallèlement à la création des fonctionnalités pour vérifier qu'elles fonctionnent bien tout au long du processus de développement.
Résumé
- Utilisez la configuration
testImplementation
pour indiquer que les dépendances s'appliquent au code source du test local et non au code de l'application. - Essayez de classer les tests selon trois scénarios : réussite, erreur et cas limite.
- Un test unitaire efficace doit remplir au moins quatre conditions : il doit être ciblé, compréhensible, déterministe et autonome.
- Les méthodes de test sont exécutées séparément pour éviter les effets secondaires inattendus pouvant découler de l'état de l'instance de test modifiable.
- Par défaut, avant l'exécution de chaque méthode de test, JUnit crée une instance de la classe de test.
- La couverture de code joue un rôle essentiel pour déterminer si vous testez correctement les classes, les méthodes et les lignes de code qui composent votre application.