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:
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:
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.
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 auxiliargetUnscrambledWord()
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:
- Abre el archivo
build.gradle.kts
del móduloapp
, ubicado en el directorioapp
del panel Project.
- Dentro del archivo, desplázate hacia abajo hasta encontrar el bloque
dependencies{}
. Agrega una dependencia con la configuración detestImplementation
parajunit
.
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("junit:junit:4.13.2")
}
- 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:
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
yisGuessedWordWrong
se actualiza correctamente.
Para crear la prueba, completa los siguientes pasos:
- 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:
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.
- En el cuerpo de la clase
GameViewModelTest
, declara una propiedadviewModel
y asígnale una instancia de la claseGameViewModel
.
class GameViewModelTest {
private val viewModel = GameViewModel()
}
- 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() {
}
}
- 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.
- En el cuerpo de la función, crea una variable
currentGameUiState
y asígnaleviewModel.uiState.value
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
}
- Con el fin de obtener la respuesta correcta del jugador, usa la función
getUnscrambledWord()
, que toma el elementocurrentGameUiState.currentScrambledWord
como argumento y muestra la palabra ordenada. Almacena el valor que se muestra en una nueva variable de solo lectura llamadacorrectPlayerWord
y asigna el valor que muestra la funcióngetUnscrambledWord()
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Para verificar si el intento del usuario es correcto, agrega una llamada al método
viewModel.updateUserGuess()
y pasa la variablecorrectPlayerWord
como argumento. Luego, agrega una llamada al métodoviewModel.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.
- Obtén la instancia de la clase
GameUiState
del valor de la propiedadviewModel.uiState
y almacénala en 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
}
- 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 propiedadcurrentGameUiState.isGuessedWordWrong
esfalse
y la funciónassertEquals()
para confirmar que el valor de la propiedadcurrentGameUiState.score
es igual a20
.
@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)
}
- Importa lo siguiente:
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
- Para que el valor
20
sea legible y reutilizable, crea un objeto complementario y asigna20
a una constanteprivate
llamadaSCORE_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
}
}
- 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:
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 entrue
porque el intento no es correcto.
Para crear la prueba, completa los siguientes pasos:
- En el cuerpo de la clase
GameViewModelTest
, crea una funcióngameViewModel_IncorrectGuess_ErrorFlagSet()
y anótala con la anotación@Test
.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
}
- 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"
}
- Agrega una llamada al método
viewModel.updateUserGuess()
y pasa la variableincorrectPlayerWord
como argumento. - 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()
}
- Agrega una variable
currentGameUiState
y asígnale el valor del estadoviewModel.uiState.value
. - Usa funciones de aserción para confirmar que el valor de la propiedad
currentGameUiState.score
es0
y que el valor de la propiedadcurrentGameUiState.isGuessedWordWrong
se estableció entrue
.
@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)
}
- Importa lo siguiente:
import org.junit.Assert.assertTrue
- 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ó en1
. - La propiedad
score
se estableció en0
. - La propiedad
isGuessedWordWrong
se estableció enfalse
. - La propiedad
isGameOver
se estableció enfalse
.
Para agregar la prueba, completa los siguientes pasos:
- Crea un método
gameViewModel_Initialization_FirstWordLoaded()
y anótalo con la anotación@Test
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
}
- Accede a la propiedad
viewModel.uiState.value
para obtener la instancia inicial de la claseGameUiState
. Asígnala a una nueva variable de solo lecturagameUiState
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
}
- Con el fin de obtener la respuesta correcta del jugador, usa la función
getUnscrambledWord()
, que toma la palabragameUiState.currentScrambledWord
y muestra la palabra ordenada. Asigna el valor que se muestra a una nueva variable de solo lectura llamadaunScrambledWord
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
}
- Verifica que el estado sea correcto agregando las funciones
assertTrue()
para confirmar que la propiedadcurrentWordCount
esté configurada como1
y la propiedadscore
, como0
. - Agrega funciones
assertFalse()
para verificar que la propiedadisGuessedWordWrong
seafalse
y que la propiedadisGameOver
esté configurada comofalse
.
@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)
}
- Importa lo siguiente:
import org.junit.Assert.assertNotEquals
- 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 constanteMAX_NO_OF_WORDS
. - La propiedad
currentGameUiState.isGameOver
está configurada comotrue
.
Para agregar la prueba, completa los siguientes pasos:
- Crea un método
gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly()
y anótalo con la anotación@Test
. En el método, crea una variableexpectedScore
y asígnale0
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
}
- Para obtener el estado inicial, agrega una variable
currentGameUiState
y asígnale el valor de la propiedadviewModel.uiState.value
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
}
- Con el fin de obtener la respuesta correcta del jugador, usa la función
getUnscrambledWord()
, que toma la palabracurrentGameUiState.currentScrambledWord
y muestra la palabra ordenada. Almacena el valor que se muestra en una nueva variable de solo lectura llamadacorrectPlayerWord
y asigna el valor que muestra la funcióngetUnscrambledWord()
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Para probar si el usuario adivina todas las respuestas, usa un bloque
repeat
y repite la ejecución de los métodosviewModel.updateUserGuess()
yviewModel.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) {
}
}
- En el bloque
repeat
, agrega el valor de la constanteSCORE_INCREASE
a la variableexpectedScore
para afirmar que la puntuación aumenta después de cada respuesta correcta. - Agrega una llamada al método
viewModel.updateUserGuess()
y pasa la variablecorrectPlayerWord
como argumento. - 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()
}
}
- Actualiza la palabra actual del jugador con la función
getUnscrambledWord()
, que toma el elementocurrentGameUiState.currentScrambledWord
como argumento y muestra la palabra ordenada. Almacena este valor que se muestra en una nueva variable de solo lectura llamadacorrectPlayerWord.
. Para verificar que el estado sea correcto, agrega la funciónassertEquals()
para comprobar si el valor de la propiedadcurrentGameUiState.score
es igual al valor deexpectedScore
.
@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)
}
}
- Agrega una función
assertEquals()
para confirmar que el valor de la propiedadcurrentGameUiState.currentWordCount
es igual al de la constanteMAX_NO_OF_WORDS
, y que el valor de la propiedadcurrentGameUiState.isGameOver
se estableció entrue
.
@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)
}
- Importa lo siguiente:
import com.example.unscramble.data.MAX_NO_OF_WORDS
- 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 pruebagameViewModel_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:
- Haz clic con el botón derecho en el archivo
GameViewModelTest.kt
del panel del proyecto y selecciona Run 'GameViewModelTest' with Coverage.
- Una vez finalizada la ejecución de prueba, en el panel de cobertura de la derecha, haz clic en la opción Flatten Packages.
- Observa el paquete
com.example.android.unscramble.ui
, como se muestra en la siguiente imagen.
- Si haces doble clic en el nombre del paquete
com.example.android.unscramble.ui
, se muestra la cobertura deGameViewModel
, como sucede en la siguiente imagen:
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.
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.
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.
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:
- Haz clic con el botón derecho en el archivo
GameViewModelTest.kt
y, en el menú, selecciona Run 'GameViewModelTest' with Coverage. - 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.
- Navega al archivo
GameViewModel.kt
y desplázate hacia abajo para verificar si ya se abarcó la ruta que antes se había omitido.
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.
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.