Cómo escribir pruebas de unidades para ViewModel

1. Antes de comenzar

En este codelab, aprenderás a escribir pruebas de unidades para probar el componente ViewModel. Agregarás pruebas de unidades para la app de juego Unscramble. Esta app es un divertido juego de palabras en el que los usuarios deben adivinar una palabra desordenada y ganan puntos por adivinar correctamente. En la siguiente imagen, se muestra una vista previa de la app:

bb1e97c357603a27.png

En el codelab Cómo escribir pruebas automatizadas, aprendiste qué son estas pruebas y por qué son importantes. También aprendiste a implementar pruebas de unidades.

Aprendiste lo siguiente:

  • Las pruebas automatizadas consisten en un código que verifica la exactitud de otro fragmento de código.
  • Probar la app es una parte importante del proceso de desarrollo. Cuando ejecutas pruebas de la app de manera constante, puedes verificar el comportamiento funcional y la usabilidad de la app antes de lanzarla públicamente.
  • Con las pruebas de unidades, puedes probar funciones, clases y propiedades.
  • Las pruebas de unidades locales se ejecutan en tu estación de trabajo, lo que significa que se ejecutan en un entorno de desarrollo sin la necesidad de contar con un dispositivo Android o emulador. En otras palabras, las pruebas locales se ejecutan en tu computadora.

Antes de continuar, asegúrate de completar los codelabs Cómo escribir pruebas automatizadas y ViewModel y el estado en Compose.

Requisitos previos

  • Conocimientos sobre Kotlin, incluidas funciones, lambdas y elementos sin estado componibles
  • Conocimientos básicos de compilación de diseños en Jetpack Compose
  • Conocimientos básicos de Material Design
  • Conocimientos básicos para implementar ViewModel

Qué aprenderás

  • Cómo agregar dependencias para pruebas de unidades en el archivo build.gradle.kts del módulo de la app
  • Cómo crear una estrategia de prueba para implementar pruebas de unidades
  • Cómo escribir pruebas de unidades con JUnit4 y comprender el ciclo de vida de las instancias de prueba
  • Cómo ejecutar, analizar y mejorar la cobertura de código

Qué compilarás

  • Pruebas de unidades para la app de juego Unscramble

Requisitos

  • La versión más reciente de Android Studio

Obtén el código de partida

Para comenzar, descarga el código de partida:

Descargar ZIP

Como alternativa, puedes clonar el repositorio de GitHub para el código:

$ 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

Puedes explorar el código en el repositorio de GitHub de Unscramble.

2. Descripción general del código de partida

En la Unidad 2, aprendiste a colocar el código de prueba de unidades en el conjunto de orígenes de prueba que se encuentra en la carpeta src, como se muestra en la siguiente imagen.

1a2dceb0dd9c618d.png

El código de partida tiene el siguiente archivo:

  • WordsData.kt: Este archivo contiene una lista de palabras para usar en la prueba y una función auxiliar getUnscrambledWord() a fin de obtener la palabra ordenada a partir de la desordenada. No es necesario modificar este archivo.

3. Cómo agregar dependencias de prueba

En este codelab, usarás el framework de JUnit para escribir pruebas de unidades. Para usar el framework, debes agregarlo como una dependencia en el archivo build.gradle.kts del módulo de tu app.

Usa la configuración implementation para especificar las dependencias que requiere tu app. Por ejemplo, para usar la biblioteca de ViewModel en tu aplicación, debes agregar una dependencia a androidx.lifecycle:lifecycle-viewmodel-compose, como se muestra en el siguiente fragmento de código:

dependencies {

    ...
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}

Ahora puedes usar esta biblioteca en el código fuente de tu app, y Android Studio te ayudará a agregarla al archivo de paquete de la aplicación (APK) generado. Sin embargo, te recomendamos que no incluyas el código de prueba de la unidad en el archivo APK, ya que no agrega ninguna funcionalidad que el usuario aprovecharía y, además, afecta el tamaño del APK. Lo mismo sucede con las dependencias que requiere el código de prueba: debes mantenerlas separadas. Para hacerlo, usa la configuración testImplementation, que indica que la configuración se aplica al código fuente de prueba local y no al código de la aplicación.

Para agregar una dependencia a tu proyecto, especifica una configuración de dependencias, como implementation o testImplementation, en el bloque de dependencias de tu archivo build.gradle.kts. La configuración de cada dependencia le proporciona a Gradle diferentes instrucciones para usarla.

Para agregar una dependencia, haz lo siguiente:

  1. Abre el archivo build.gradle.kts del módulo app, ubicado en el directorio app del panel Project.

bc235c0754e4e0f2.png

  1. Dentro del archivo, desplázate hacia abajo hasta encontrar el bloque dependencies{}. Agrega una dependencia con la configuración de testImplementation para junit.
plugins {
    ...
}

android {
    ...
}

dependencies {
    ...
    testImplementation("junit:junit:4.13.2")
}
  1. En la barra de notificaciones que se encuentre en la parte superior del archivo build.gradle.kts, haz clic en Sync Now para permitir que la importación y la compilación finalicen, como se muestra en la siguiente captura de pantalla:

1c20fc10750ca60c.png

Lista de materiales (BoM) de Compose

La BoM de Compose es la forma recomendada de administrar las versiones de la biblioteca de Compose. Especifica solo la versión de la BoM, y esta te permitirá administrar todas las versiones de tu biblioteca de Compose.

Observa la sección de dependencia en el archivo build.gradle.kts del módulo 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")
    ...
}

Mira lo siguiente:

  • No se especificaron los números de versión de la biblioteca de Compose.
  • La BoM se importa con implementation platform("androidx.compose:compose-bom:2023.06.01").

Esto se debe a que la BoM en sí tiene vínculos a las versiones estables más recientes de las diferentes bibliotecas de Compose, de modo que funcionen bien juntas. Si la usas en tu app, no necesitas agregar ninguna versión a las dependencias de la biblioteca de Compose. Cuando actualizas la versión de BoM, todas las bibliotecas que usas se actualizan automáticamente a sus versiones nuevas.

Para usar la BoM con las bibliotecas de pruebas de Compose (pruebas de instrumentación), debes importar androidTestImplementation platform("androidx.compose:compose-bom:xxxx.xx.xx"). Puedes crear una variable y reutilizarla en la implementation y la androidTestImplementation como se muestra.

// 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")

}

¡Perfecto! Agregaste correctamente las dependencias de prueba a la app y aprendiste sobre las BoM. Ya puedes agregar algunas pruebas de unidades.

4. Estrategia de prueba

Una buena estrategia de prueba consiste en tener en cuenta diferentes rutas y límites de tu código. En un nivel muy básico, puedes categorizar las pruebas en tres situaciones: ruta de éxito, ruta de error y caso límite.

  • Ruta de éxito: Las pruebas de la ruta de éxito, también conocidas como pruebas de ruta viables, se enfocan en probar la funcionalidad de un flujo positivo. Un flujo positivo es aquel que no tiene condiciones de excepción o de error. En comparación con las situaciones de ruta de error y caso límite, resulta fácil crear una lista exhaustiva de situaciones para rutas de éxito, ya que se enfocan en el comportamiento previsto para tu app.

Un ejemplo de una ruta de éxito en la app de Unscramble es la actualización correcta de la puntuación, la cantidad de palabras y la palabra desordenada cuando el usuario ingresa una palabra correcta y hace clic en el botón Submit.

  • Ruta de error: Las pruebas de la ruta de error se enfocan en evaluar la funcionalidad de un flujo negativo, es decir, verificar cómo responde la app ante condiciones de error o entradas del usuario no válidas. Es bastante difícil determinar todos los flujos de errores posibles porque existen muchos resultados alternativos cuando no se logra el comportamiento previsto.

Un consejo general consiste en enumerar todas las rutas de error posibles, escribir pruebas para ellas y actualizar tus pruebas de unidades a medida que descubres diferentes situaciones.

Un ejemplo de una ruta de error en la app de Unscramble se da cuando el usuario ingresa una palabra incorrecta y hace clic en el botón Submit, lo que hace que aparezca un mensaje de error y que la puntuación y la cantidad de palabras no se actualicen.

  • Caso límite: Un caso límite se enfoca en probar condiciones límite en una app. En la app de Unscramble, un límite consiste en verificar el estado de la IU cuando la app se carga y después de que el usuario intenta una cantidad máxima de palabras.

Crear situaciones de prueba en torno a estas categorías puede servir como guía para tu plan de prueba.

Cómo crear pruebas

Por lo general, una buena prueba de unidades tiene las siguientes cuatro propiedades:

  • Enfoque: Debe enfocarse en probar una unidad, como un fragmento de código. Este fragmento suele ser una clase o un método. La prueba debe ser limitada y enfocarse en validar la precisión de los fragmentos de código individuales, en lugar de varios fragmentos al mismo tiempo.
  • Comprensible: El código debe ser simple y fácil de entender. A simple vista, un desarrollador debe poder comprender de inmediato la intención detrás de la prueba.
  • Determinista: La ejecución de la prueba debe pasar o fracasar de manera constante. Se debería generar el mismo resultado siempre que ejecutes las pruebas sin hacer cambios en el código, independientemente de la cantidad de veces que las ejecutes. La prueba no debe ser inestable; es decir, no debe fracasar en una instancia y pasar en otra si no se modificó el código.
  • Independiente: La prueba se ejecuta de forma aislada y no requiere que una persona la configure ni interactúe con ella.

Ruta de éxito

A fin de escribir una prueba de unidades para la ruta de éxito, debes establecer una aserción que indique que, dado que se inicializó una instancia del GameViewModel, cuando se llama al método updateUserGuess() con la palabra correcta, seguida de una llamada al método checkUserGuess(), sucederá lo siguiente:

  • Se pasa el intento correcto al método updateUserGuess().
  • Se llama al método checkUserGuess().
  • El valor de los estados score y isGuessedWordWrong se actualiza correctamente.

Para crear la prueba, completa los siguientes pasos:

  1. Crea un nuevo paquete com.example.android.unscramble.ui.test en el conjunto de orígenes de prueba y agrega el archivo como se muestra en la siguiente captura de pantalla:

57d004ccc4d75833.png

f98067499852bdce.png

Para escribir una prueba de unidades para la clase GameViewModel, necesitas una instancia de la clase de modo que puedas llamar a los métodos correspondientes y verificar el estado.

  1. En el cuerpo de la clase GameViewModelTest, declara una propiedad viewModel y asígnale una instancia de la clase GameViewModel.
class GameViewModelTest {
    private val viewModel = GameViewModel()
}
  1. Si quieres escribir una prueba de unidades para la ruta de éxito, crea una función gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() y anótala con la anotación @Test.
class GameViewModelTest {
    private val viewModel = GameViewModel()

    @Test
    fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset()  {
    }
}
  1. Importa lo siguiente:
import org.junit.Test

A los efectos de pasar una palabra correcta del jugador al método viewModel.updateUserGuess(), debes obtener la palabra ordenada correcta a partir de la desordenada en GameUiState. Para ello, primero obtén el estado actual de la IU del juego.

  1. En el cuerpo de la función, crea una variable currentGameUiState y asígnale viewModel.uiState.value.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
}
  1. Con el fin de obtener la respuesta correcta del jugador, usa la función getUnscrambledWord(), que toma el elemento currentGameUiState.currentScrambledWord como argumento y muestra la palabra ordenada. Almacena el valor que se muestra en una nueva variable de solo lectura llamada correctPlayerWord y asigna el valor que muestra la función getUnscrambledWord().
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

}
  1. Para verificar si el intento del usuario es correcto, agrega una llamada al método viewModel.updateUserGuess() y pasa la variable correctPlayerWord como argumento. Luego, agrega una llamada al método viewModel.checkUserGuess() y verifica la respuesta.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

    viewModel.updateUserGuess(correctPlayerWord)
    viewModel.checkUserGuess()
}

Ahora está todo listo para confirmar que el estado del juego es lo que esperas.

  1. Obtén la instancia de la clase GameUiState del valor de la propiedad viewModel.uiState y almacénala en 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. Para verificar si el intento del usuario es correcto y si se actualiza la puntuación, usa la función assertFalse() para comprobar que la propiedad currentGameUiState.isGuessedWordWrong es false y la función assertEquals() para confirmar que el valor de la propiedad currentGameUiState.score es igual a 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. Importa lo siguiente:
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
  1. Para que el valor 20 sea legible y reutilizable, crea un objeto complementario y asigna 20 a una constante private llamada SCORE_AFTER_FIRST_CORRECT_ANSWER. Actualiza la prueba con la constante recién creada.
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. Ejecuta la prueba.

La prueba debería ser exitosa, ya que todas las aserciones eran válidas, como se muestra en la siguiente captura de pantalla:

c412a2ac3fbefa57.png

Ruta de error

A fin de escribir una prueba de unidades para la ruta de error, debes establecer una aserción que indique que, cuando se pasa una palabra incorrecta como argumento al método viewModel.updateUserGuess() y se llama al método viewModel.checkUserGuess(), sucede lo siguiente:

  • El valor de la propiedad currentGameUiState.score no se modifica.
  • El valor de la propiedad currentGameUiState.isGuessedWordWrong se establece en true porque el intento no es correcto.

Para crear la prueba, completa los siguientes pasos:

  1. En el cuerpo de la clase GameViewModelTest, crea una función gameViewModel_IncorrectGuess_ErrorFlagSet() y anótala con la anotación @Test.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {

}
  1. Define una variable incorrectPlayerWord y asígnale el valor "and", que no debe existir en la lista de palabras.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
    // Given an incorrect word as input
    val incorrectPlayerWord = "and"
}
  1. Agrega una llamada al método viewModel.updateUserGuess() y pasa la variable incorrectPlayerWord como argumento.
  2. Agrega una llamada al método viewModel.checkUserGuess() para verificar la respuesta.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
    // Given an incorrect word as input
    val incorrectPlayerWord = "and"

    viewModel.updateUserGuess(incorrectPlayerWord)
    viewModel.checkUserGuess()
}
  1. Agrega una variable currentGameUiState y asígnale el valor del estado viewModel.uiState.value.
  2. Usa funciones de aserción para confirmar que el valor de la propiedad currentGameUiState.score es 0 y que el valor de la propiedad currentGameUiState.isGuessedWordWrong se estableció en 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. Importa lo siguiente:
import org.junit.Assert.assertTrue
  1. Ejecuta la prueba para confirmar que se complete con éxito.

Caso límite

Para probar el estado inicial de la IU, debes escribir una prueba de unidades para la clase GameViewModel. La prueba debe afirmar que, cuando se inicializa GameViewModel, se cumple lo siguiente:

  • La propiedad currentWordCount se estableció en 1.
  • La propiedad score se estableció en 0.
  • La propiedad isGuessedWordWrong se estableció en false.
  • La propiedad isGameOver se estableció en false.

Para agregar la prueba, completa los siguientes pasos:

  1. Crea un método gameViewModel_Initialization_FirstWordLoaded() y anótalo con la anotación @Test.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {

}
  1. Accede a la propiedad viewModel.uiState.value para obtener la instancia inicial de la clase GameUiState. Asígnala a una nueva variable de solo lectura gameUiState.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
}
  1. Con el fin de obtener la respuesta correcta del jugador, usa la función getUnscrambledWord(), que toma la palabra gameUiState.currentScrambledWord y muestra la palabra ordenada. Asigna el valor que se muestra a una nueva variable de solo lectura llamada unScrambledWord.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

}
  1. Verifica que el estado sea correcto agregando las funciones assertTrue() para confirmar que la propiedad currentWordCount esté configurada como 1 y la propiedad score, como 0.
  2. Agrega funciones assertFalse() para verificar que la propiedad isGuessedWordWrong sea false y que la propiedad isGameOver esté configurada como 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. Importa lo siguiente:
import org.junit.Assert.assertNotEquals
  1. Ejecuta la prueba para confirmar que se complete con éxito.

Otro caso límite es probar el estado de la IU una vez que el usuario adivine todas las palabras. Debes establecer una aserción que indique que, cuando el usuario adivina todas las palabras correctamente, se cumple lo siguiente:

  • La puntuación está actualizada.
  • La propiedad currentGameUiState.currentWordCount es igual al valor de la constante MAX_NO_OF_WORDS.
  • La propiedad currentGameUiState.isGameOver está configurada como true.

Para agregar la prueba, completa los siguientes pasos:

  1. Crea un método gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() y anótalo con la anotación @Test. En el método, crea una variable expectedScore y asígnale 0.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
}
  1. Para obtener el estado inicial, agrega una variable currentGameUiState y asígnale el valor de la propiedad viewModel.uiState.value.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
}
  1. Con el fin de obtener la respuesta correcta del jugador, usa la función getUnscrambledWord(), que toma la palabra currentGameUiState.currentScrambledWord y muestra la palabra ordenada. Almacena el valor que se muestra en una nueva variable de solo lectura llamada correctPlayerWord y asigna el valor que muestra la función getUnscrambledWord().
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
  1. Para probar si el usuario adivina todas las respuestas, usa un bloque repeat y repite la ejecución de los métodos viewModel.updateUserGuess() y viewModel.checkUserGuess() MAX_NO_OF_WORDS veces.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
    repeat(MAX_NO_OF_WORDS) {

    }
}
  1. En el bloque repeat, agrega el valor de la constante SCORE_INCREASE a la variable expectedScore para afirmar que la puntuación aumenta después de cada respuesta correcta.
  2. Agrega una llamada al método viewModel.updateUserGuess() y pasa la variable correctPlayerWord como argumento.
  3. Agrega una llamada al método viewModel.checkUserGuess() para activar la verificación del intento del usuario.
@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. Actualiza la palabra actual del jugador con la función getUnscrambledWord(), que toma el elemento currentGameUiState.currentScrambledWord como argumento y muestra la palabra ordenada. Almacena este valor que se muestra en una nueva variable de solo lectura llamada correctPlayerWord.. Para verificar que el estado sea correcto, agrega la función assertEquals() para comprobar si el valor de la propiedad currentGameUiState.score es igual al valor de 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. Agrega una función assertEquals() para confirmar que el valor de la propiedad currentGameUiState.currentWordCount es igual al de la constante MAX_NO_OF_WORDS, y que el valor de la propiedad currentGameUiState.isGameOver se estableció en 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. Importa lo siguiente:
import com.example.unscramble.data.MAX_NO_OF_WORDS
  1. Ejecuta la prueba para confirmar que se complete con éxito.

Descripción general del ciclo de vida de la instancia de prueba

Si observas la forma en que se inicializa viewModel en la prueba, es posible que notes que viewModel se inicializa una sola vez, aunque todas las pruebas lo usen. En este fragmento de código, se muestra la definición de la propiedad viewModel.

class GameViewModelTest {
    private val viewModel = GameViewModel()

    @Test
    fun gameViewModel_Initialization_FirstWordLoaded() {
        val gameUiState = viewModel.uiState.value
        ...
    }
    ...
}

Es posible que te hagas las siguientes preguntas:

  • ¿Significa que la misma instancia de viewModel se vuelve a usar para todas las pruebas?
  • ¿Provocará algún problema? Por ejemplo, ¿qué sucede si el método de prueba gameViewModel_Initialization_FirstWordLoaded se ejecuta después del método de prueba gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset? ¿Fallará la prueba de inicialización?

La respuesta a ambas preguntas es no. Los métodos de prueba se ejecutan de forma aislada para evitar efectos secundarios inesperados del estado de instancia de la prueba mutable. De forma predeterminada, antes de ejecutar cada método de prueba, JUnit crea una nueva instancia de la clase de prueba.

Como hasta ahora tienes cuatro métodos de prueba en tu clase GameViewModelTest, GameViewModelTest crea una instancia cuatro veces. Cada instancia tiene su propia copia de la propiedad viewModel. Por lo tanto, la secuencia de ejecución de prueba no importa.

5. Introducción a la cobertura de código

La cobertura de código resulta esencial para determinar si estás probando de forma adecuada las clases, los métodos y las líneas de código que forman parte de tu app.

Android Studio proporciona una herramienta de cobertura de prueba para las pruebas de unidades locales de modo tal que puedas realizar un seguimiento del porcentaje y las áreas del código de tu app que abarcaron las pruebas de tu unidad.

Cómo ejecutar pruebas con cobertura mediante Android Studio

Para ejecutar pruebas con cobertura, haz lo siguiente:

  1. Haz clic con el botón derecho en el archivo GameViewModelTest.kt del panel del proyecto y selecciona cf4c5adfe69a119f.png Run 'GameViewModelTest' with Coverage.

73545d5ade3851df.png

  1. Una vez finalizada la ejecución de prueba, en el panel de cobertura de la derecha, haz clic en la opción Flatten Packages.

90e2989f8b58d254.png

  1. Observa el paquete com.example.android.unscramble.ui, como se muestra en la siguiente imagen.

1c755d17d19c6f65.png

  1. Si haces doble clic en el nombre del paquete com.example.android.unscramble.ui, se muestra la cobertura de GameViewModel, como sucede en la siguiente imagen:

14cf6ca3ffb557c4.png

Cómo analizar el informe de prueba

El informe que se muestra en el siguiente diagrama se divide en dos aspectos:

  • El porcentaje de métodos que abarcan las pruebas de unidades: En el diagrama de ejemplo, las pruebas que escribiste hasta el momento abarcaron 7 de 8 métodos. Esto equivale al 87% del total de métodos.
  • El porcentaje de líneas que abarcan las pruebas de unidades: En el diagrama de ejemplo, las pruebas que escribiste abarcaron 39 de las 41 líneas de código. Esto equivale al 95% de las líneas de código.

Los informes sugieren que las pruebas de unidades que escribiste hasta ahora omitieron ciertas partes del código. Para determinar qué partes se perdieron, completa el siguiente paso:

  • Haz doble clic en GameViewModel.

c934ba14e096bddd.png

Android Studio muestra el archivo GameViewModel.kt con codificación de color adicional en el lado izquierdo de la ventana. El color verde brillante indica que esas líneas de código estaban cubiertas.

edc4e5faf352119b.png

Cuando te desplaces hacia abajo en GameViewModel, es posible que notes que algunas líneas están marcadas con un color rosa claro. Este color indica que estas líneas de código no estaban cubiertas por las pruebas de unidades.

6df985f713337a0c.png

Cómo mejorar la cobertura

A fin de mejorar la cobertura, debes escribir una prueba que abarque la ruta faltante. Debes agregar una prueba para establecer una aserción que indique que, cuando un usuario omite una palabra, se cumple lo siguiente:

  • La propiedad currentGameUiState.score no se modifica.
  • La propiedad currentGameUiState.currentWordCount se incrementa en una unidad, como se muestra en el siguiente fragmento de código.

Prepárate para mejorar la cobertura. Para ello, agrega el siguiente método de prueba a la clase 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)
}

Para volver a ejecutar la cobertura, sigue estos pasos:

  1. Haz clic con el botón derecho en el archivo GameViewModelTest.kt y, en el menú, selecciona Run 'GameViewModelTest' with Coverage.
  2. Una vez que la compilación se complete correctamente, vuelve a navegar al elemento GameViewModel y confirma que el porcentaje de cobertura sea del 100%. El informe de cobertura final se muestra en la siguiente imagen.

145781df2c68f71c.png

  1. Navega al archivo GameViewModel.kt y desplázate hacia abajo para verificar si ya se abarcó la ruta que antes se había omitido.

357263bdb9219779.png

Aprendiste a ejecutar, analizar y mejorar la cobertura de código de tu aplicación.

¿Un porcentaje de cobertura de código alto significa una calidad alta del código de la app? No. La cobertura de código indica el porcentaje de código cubierto o ejecutado por tu prueba de unidades. No indica que el código esté verificado. Si quitas todas las aserciones de tu código de prueba de unidades y ejecutas la cobertura de código, seguirá apareciendo una cobertura del 100%.

Una cobertura alta no indica que las pruebas estén diseñadas correctamente ni que verifiquen el comportamiento de la app. Asegúrate de que las pruebas que escribiste tengan las aserciones que verifican el comportamiento de la clase que se está probando. Tampoco necesitas esforzarte por escribir pruebas de unidades para obtener una cobertura de prueba del 100% para toda la app, sino que debes probar algunas partes del código, como las Actividades, con pruebas de IU.

Sin embargo, una cobertura baja significa que grandes partes de tu código quedaron totalmente sin probarse. Usa la cobertura de código como una herramienta para encontrar las partes de código que tus pruebas no hayan ejecutado, en lugar de considerarla una herramienta para medir la calidad de tu código.

6. Obtén el código de la solución

Para descargar el código del codelab terminado, puedes usar estos comandos de git:

$ 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

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución, puedes hacerlo en GitHub.

7. Conclusión

¡Felicitaciones! Aprendiste a definir una estrategia de prueba y, además, implementaste pruebas de unidades para probar ViewModel y StateFlow en la app de Unscramble. A medida que continúes compilando apps para Android, asegúrate de escribir pruebas junto con las funciones de tu app para confirmar que estas funcionen correctamente durante el proceso de desarrollo.

Resumen

  • Usa la configuración testImplementation para indicar que las dependencias se aplican al código fuente de prueba local y no al código de la aplicación.
  • Intenta categorizar las pruebas en tres situaciones: ruta de éxito, ruta de error y caso límite.
  • Una buena prueba de unidades tiene al menos cuatro características: son enfocadas, comprensibles, determinísticas e independientes.
  • Los métodos de prueba se ejecutan de forma aislada para evitar efectos secundarios inesperados del estado de instancia de la prueba mutable.
  • De forma predeterminada, antes de que se ejecute cada método de prueba, JUnit crea una nueva instancia de la clase de prueba.
  • La cobertura de código resulta esencial para determinar si probaste de forma adecuada las clases, los métodos y las líneas de código que forman parte de tu app.

Más información