Écrire des tests unitaires pour ViewModel

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 :

bb1e97c357603a27.png

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 :

1a2dceb0dd9c618d.png

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'assistance getUnscrambledWord() 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 :

  1. Ouvrez le fichier build.gradle.kts du module app, situé dans le répertoire app du volet Project (Projet).

bc235c0754e4e0f2.png

  1. Faites défiler le fichier vers le bas jusqu'au bloc dependencies{}. Ajoutez une dépendance pour junit à l'aide de la configuration testImplementation.
plugins {
    ...
}

android {
    ...
}

dependencies {
    ...
    testImplementation("junit:junit:4.13.2")
}
  1. 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 :

1c20fc10750ca60c.png

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 et isGuessedWordWrong est mise à jour correctement.

Pour créer le test, procédez comme suit :

  1. 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 :

57d004ccc4d75833.png

f98067499852bdce.png

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.

  1. Dans le corps de la classe GameViewModelTest, déclarez une propriété viewModel et attribuez-lui une instance de la classe GameViewModel.
class GameViewModelTest {
    private val viewModel = GameViewModel()
}
  1. 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()  {
    }
}
  1. 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.

  1. Dans le corps de la fonction, créez une variable currentGameUiState et attribuez-lui viewModel.uiState.value.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
}
  1. Pour obtenir le mot correct deviné par le joueur, utilisez la fonction getUnscrambledWord(), qui prend currentGameUiState.currentScrambledWord comme argument et renvoie le mot réel. Stockez cette valeur renvoyée dans une nouvelle variable en lecture seule nommée correctPlayerWord et attribuez la valeur renvoyée par la fonction getUnscrambledWord().
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

}
  1. Pour vérifier si le mot deviné est correct, ajoutez un appel à la méthode viewModel.updateUserGuess() et transmettez la variable correctPlayerWord en tant qu'argument. Ajoutez ensuite un appel à la méthode viewModel.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.

  1. Récupérez l'instance de la classe GameUiState à partir de la valeur de la propriété viewModel.uiState, puis stockez-la dans la variable currentGameUiState.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    viewModel.updateUserGuess(correctPlayerWord)
    viewModel.checkUserGuess()

    currentGameUiState = viewModel.uiState.value
}
  1. 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 est false, et la fonction assertEquals() 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)
}
  1. Exécutez la commande d'importation suivante :
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
  1. Pour rendre la valeur 20 lisible et réutilisable, créez un objet associé et attribuez 20 à une constante private nommée SCORE_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
    }
}
  1. Exécutez le test.

Il devrait réussir, car toutes les assertions sont valides, comme illustré dans la capture d'écran suivante :

c412a2ac3fbefa57.png

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 sur true, car le mot deviné est incorrect.

Pour créer le test, procédez comme suit :

  1. Dans le corps de la classe GameViewModelTest, créez une fonction gameViewModel_IncorrectGuess_ErrorFlagSet() et marquez-la avec l'annotation @Test.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {

}
  1. 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"
}
  1. Ajoutez un appel à la méthode viewModel.updateUserGuess() et transmettez la variable incorrectPlayerWord en tant qu'argument.
  2. 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()
}
  1. Ajoutez une variable currentGameUiState et attribuez-lui la valeur de l'état viewModel.uiState.value.
  2. Utilisez des fonctions d'assertion pour confirmer que la valeur de la propriété currentGameUiState.score est 0 et que la valeur de la propriété currentGameUiState.isGuessedWordWrong est définie sur true.
@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)
}
  1. Exécutez la commande d'importation suivante :
import org.junit.Assert.assertTrue
  1. 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 sur 1.
  • La propriété score est définie sur 0.
  • La propriété isGuessedWordWrong est définie sur false.
  • La propriété isGameOver est définie sur false.

Pour ajouter le test, procédez comme suit :

  1. Créez une méthode gameViewModel_Initialization_FirstWordLoaded() et marquez-la avec l'annotation @Test.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {

}
  1. Accédez à la propriété viewModel.uiState.value pour obtenir l'instance initiale de la classe GameUiState. Attribuez-la à une nouvelle variable gameUiState en lecture seule.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
}
  1. Pour obtenir le mot correct deviné par le joueur, utilisez la fonction getUnscrambledWord(), qui récupère le mot gameUiState.currentScrambledWord et renvoie le mot réel. Attribuez la valeur renvoyée à une nouvelle variable en lecture seule nommée unScrambledWord.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

}
  1. Pour vérifier que l'état est correct, ajoutez les fonctions assertTrue() afin de confirmer que la propriété currentWordCount est définie sur 1 et que la propriété score est définie sur 0.
  2. Ajoutez des fonctions assertFalse() pour vérifier que la propriété isGuessedWordWrong est false et que la propriété isGameOver est définie sur false.
@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)
}
  1. Exécutez la commande d'importation suivante :
import org.junit.Assert.assertNotEquals
  1. 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 constante MAX_NO_OF_WORDS.
  • La propriété currentGameUiState.isGameOver est définie sur true.

Pour ajouter le test, procédez comme suit :

  1. Créez une méthode gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() et marquez-la avec l'annotation @Test. Dans la méthode, créez une variable expectedScore et attribuez-lui 0.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
}
  1. 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
}
  1. Pour obtenir le mot correct deviné par le joueur, utilisez la fonction getUnscrambledWord(), qui récupère le mot currentGameUiState.currentScrambledWord et renvoie le mot réel. Stockez cette valeur renvoyée dans une nouvelle variable en lecture seule nommée correctPlayerWord et attribuez la valeur renvoyée par la fonction getUnscrambledWord().
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
  1. 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éthode viewModel.updateUserGuess() et de la méthode viewModel.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) {

    }
}
  1. Dans le bloc repeat, ajoutez la valeur de la constante SCORE_INCREASE à la variable expectedScore pour indiquer que le score augmente après chaque bonne réponse.
  2. Ajoutez un appel à la méthode viewModel.updateUserGuess() et transmettez la variable correctPlayerWord en tant qu'argument.
  3. 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()
    }
}
  1. Mettez à jour le mot actuel du joueur à l'aide de la fonction getUnscrambledWord(), qui récupère currentGameUiState.currentScrambledWord comme argument et renvoie le mot réel. Stockez cette valeur renvoyée dans une nouvelle variable en lecture seule nommée correctPlayerWord.. Pour vérifier que l'état est correct, ajoutez la fonction assertEquals() afin de vérifier si la valeur de la propriété currentGameUiState.score est égale à la valeur de la variable expectedScore.
@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)
    }
}
  1. Ajoutez une fonction assertEquals() pour confirmer que la valeur de la propriété currentGameUiState.currentWordCount est égale à la valeur de la constante MAX_NO_OF_WORDS et que la valeur de la propriété currentGameUiState.isGameOver est définie sur true.
@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)
}
  1. Exécutez la commande d'importation suivante :
import com.example.unscramble.data.MAX_NO_OF_WORDS
  1. 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 test gameViewModel_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 :

  1. Effectuez un clic droit sur le fichier GameViewModelTest.kt dans le volet du projet, puis sélectionnez cf4c5adfe69a119f.png Run 'GameViewModelTest' with Coverage (Exécuter "GameViewModelTest" avec la couverture).

73545d5ade3851df.png

  1. Une fois l'exécution du test terminée, cliquez sur l'option Flatten Packages (Aplatir les packages) dans le volet de couverture à droite.

90e2989f8b58d254.png

  1. Notez le package com.example.android.unscramble.ui, comme illustré dans l'image ci-dessous.

1c755d17d19c6f65.png

  1. Double-cliquez sur le nom du package com.example.android.unscramble.ui pour afficher la couverture de GameViewModel, comme illustré dans l'image suivante :

14cf6ca3ffb557c4.png

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.

c934ba14e096bddd.png

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.

edc4e5faf352119b.png

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.

6df985f713337a0c.png

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 :

  1. 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.
  2. 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.

145781df2c68f71c.png

  1. 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.

357263bdb9219779.png

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.

En savoir plus