1. Antes de começar
Este codelab ensina a criar testes de unidade para testar o componente ViewModel
. Você vai adicionar testes de unidade para o app de jogo Unscramble. O app Unscramble é um jogo de palavras divertido em que os usuários precisam adivinhar uma palavra embaralhada e ganham pontos por acertar. A imagem abaixo mostra uma prévia do app:
No codelab Criar testes automatizados, você aprendeu o que são testes automatizados e por que eles são importantes. Você também aprendeu a implementar testes de unidade.
Você aprendeu que:
- Os testes automatizados são códigos que verificam a precisão de outro código.
- Os testes são uma parte importante do processo de desenvolvimento de apps. Executando testes no app de forma consistente, você pode verificar o comportamento funcional e a usabilidade dele antes de o lançar publicamente.
- Com testes de unidade, é possível testar funções, classes e propriedades.
- Os testes de unidade locais são executados na estação de trabalho, o que significa que eles são executados em um ambiente de desenvolvimento sem a necessidade de um dispositivo ou emulador Android. Em outras palavras, os testes locais são executados no seu computador.
Antes de continuar, conclua os codelabs Criar testes automatizados e ViewModel e estado no Compose.
Pré-requisitos
- Conhecimento sobre Kotlin, incluindo funções, lambdas e elementos combináveis sem estado.
- Conhecimento básico sobre como criar layouts no Jetpack Compose.
- Conhecimento básico do Material Design.
- Conhecimento básico sobre como implementar o ViewModel.
O que você vai aprender
- Como adicionar dependências para testes de unidade no arquivo
build.gradle.kts
do módulo do app. - Como criar uma estratégia de teste para implementar testes de unidade.
- Como criar testes de unidade usando o JUnit4 e entender o ciclo de vida da instância de teste.
- Como executar, analisar e melhorar a cobertura de código.
O que você vai criar
- Testes de unidade do app de jogo Unscramble
O que é necessário
- A versão mais recente do Android Studio.
Acessar o código inicial
Para começar, faça o download do código inicial:
Outra opção é clonar o repositório do GitHub:
$ 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
Procure o código no repositório do GitHub do Unscramble
(link em inglês).
2. Visão geral do código inicial
Na Unidade 2, você aprendeu a colocar o código do teste de unidade no conjunto de origem test, que está na pasta src, conforme mostrado na imagem:
O código inicial tem este arquivo:
WordsData.kt
: contém uma lista de palavras a serem usadas para testes e uma função auxiliargetUnscrambledWord()
para acessar a palavra desembaralhada com a palavra embaralhada. Não é necessário modificar esse arquivo.
3. Adicionar dependências de teste
Neste codelab, você vai usar o framework do JUnit para criar testes de unidade. Para usar o framework, adicione-o como uma dependência no arquivo build.gradle.kts
do módulo do seu app.
Use a configuração implementation
para especificar as dependências exigidas pelo app. Por exemplo, para usar a biblioteca ViewModel
no seu aplicativo, é preciso adicionar uma dependência a androidx.lifecycle:lifecycle-viewmodel-compose
, conforme mostrado neste snippet de código:
dependencies {
...
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}
Agora você pode usar essa biblioteca no código-fonte do app, e o Android Studio vai ajudar a adicioná-la ao arquivo de pacote de apps (APK) gerado. No entanto, não é recomendável que o código do teste de unidade faça parte do arquivo APK. O código de teste não adiciona nenhuma funcionalidade que o usuário usaria e, além disso, afeta o tamanho do APK. O mesmo vale para as dependências exigidas pelo código de teste. Eles devem ficar separados. Para fazer isso, use a configuração testImplementation
, que indica que a configuração se aplica ao código-fonte do teste local, e não ao código do aplicativo.
Para adicionar uma dependência a um projeto, especifique uma configuração de dependência, por exemplo, implementation
ou testImplementation
, no bloco de dependências do arquivo build.gradle.kts
. Cada configuração de dependência fornece ao Gradle instruções diferentes sobre como usar a dependência.
Para adicionar uma dependência, faça o seguinte:
- Abra o arquivo
build.gradle.kts
do móduloapp
, localizado no diretórioapp
no painel Project.
- Dentro do arquivo, role a tela para baixo até encontrar o bloco
dependencies{}
. Adicione uma dependência usando a configuraçãotestImplementation
parajunit
.
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("junit:junit:4.13.2")
}
- Na barra de notificações localizada na parte de cima do arquivo build.gradle.kts, clique em Sync Now para que a importação e o build sejam concluídos, conforme mostrado nesta captura de tela:
Lista de materiais (BoM) do Compose
A BoM do Compose é a maneira recomendada de gerenciar versões das bibliotecas do Compose. É possível gerenciar todas as versões das bibliotecas do Compose especificando apenas a versão da BoM.
Observe a seção de dependência no arquivo build.gradle.kts
do 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")
...
}
Perceba que:
- Os números de versão das biblioteca do Compose não são especificados.
- A BoM é importada usando
implementation platform("androidx.compose:compose-bom:2023.06.01")
.
Isso ocorre porque a própria BoM tem links para as versões estáveis mais recentes das diferentes bibliotecas do Compose, de maneira que elas funcionem bem juntas. Ao usar a BoM no app, não é necessário adicionar nenhuma versão às dependências de bibliotecas do Compose. Quando você atualiza a versão da BoM, todas as bibliotecas usadas são atualizadas de forma automática para as novas versões.
Para usar a BoM com as bibliotecas de teste do Compose (testes de instrumentação), é necessário importar o androidTestImplementation platform("androidx.compose:compose-bom:xxxx.xx.xx")
. Você pode criar e reutilizar uma variável para implementation
e androidTestImplementation
, conforme mostrado.
// 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")
}
Muito bem! Você adicionou dependências de teste ao app e aprendeu sobre a BoM. Agora está tudo pronto para adicionar alguns testes de unidade.
4. Estratégia de teste
Uma boa estratégia de teste consiste em cobrir diferentes caminhos e limites do código. Em nível muito básico, é possível categorizar os testes em três cenários: caminho de sucesso, caminho de erro e caso de limite.
- Caminho de sucesso: os testes de caminho de sucesso, também conhecidos como "testes de cenário ideal", têm como foco testar a funcionalidade de um fluxo positivo. Um fluxo positivo é aquele que não tem condições de exceção ou de erro. Em comparação com os cenários de caminho de erro e caso de limite, é fácil criar uma lista completa de cenários de caminhos de sucesso, já que eles se concentram no comportamento pretendido do app.
Um exemplo de caminho de sucesso no app Unscramble é o resultado correto da pontuação, da contagem de palavras e da palavra embaralhada quando o usuário digita uma palavra correta e clica no botão Enviar.
- Caminho de erro: os testes de caminho de erro se concentram em testar a funcionalidade de um fluxo negativo, ou seja, para verificar como o app responde a condições de erro ou a uma entrada inválida do usuário. É muito difícil determinar todos os fluxos de erro em potencial porque há muitos resultados possíveis quando o comportamento pretendido não é alcançado.
Uma recomendação geral é listar todos os caminhos de erro possíveis, programar testes para eles e manter seus testes de unidade em evolução à medida que descobrir diferentes cenários.
Um exemplo de caminho de erro no app Unscramble: o usuário digita uma palavra incorreta e clica no botão Enviar, o que faz com que uma mensagem de erro seja mostrada, e a pontuação e a contagem de palavras não sejam atualizadas.
- Caso de limite: um caso de limite se concentra em testar as condições de limite no app. No app Unscramble, um limite verifica o estado da IU quando o app é carregado e depois que o usuário reproduz um número máximo de palavras.
A criação de cenários de teste com essas categorias pode servir como um guia para seu plano de testes.
Criar testes
Um bom teste de unidade normalmente tem as seguintes propriedades:
- Focado: ele precisa se focar no teste de uma unidade, como um trecho de código. Esse código geralmente é uma classe ou um método. O teste precisa ser limitado e focado em validar a exatidão de partes individuais do código, em vez de várias partes ao mesmo tempo.
- Compreensível: o código precisa ser simples e fácil de entender. Em resumo, o desenvolvedor precisa entender imediatamente a intenção por trás do teste.
- Determinístico: ele é aprovado ou falha de forma consistente. Quando você executa os testes quantas vezes quiser, sem fazer mudanças no código, o teste precisa gerar o mesmo resultado. O teste não pode ser instável, com uma falha em uma instância e uma aprovação em outra, apesar de não haver modificação no código.
- Autônomo: não requer interação ou configuração humana e é executado de forma isolada.
Caminho de sucesso
Para criar um teste de unidade para o caminho de sucesso, é necessário declarar que, considerando que uma instância do GameViewModel
foi inicializada, quando o updateUserGuess()
é chamado com palpite da palavra correta seguida por uma chamada para checkUserGuess()
:
- o palpite correto é transmitido ao método
updateUserGuess()
; - o método tem o nome
checkUserGuess()
; - o valor dos status
score
eisGuessedWordWrong
é atualizado corretamente.
Para criar o teste, siga estas etapas:
- Crie um novo pacote
com.example.android.unscramble.ui.test
no conjunto de origem do teste e adicione o arquivo, conforme mostrado nesta captura de tela:
Para criar um teste de unidade para a classe GameViewModel
, você precisa de uma instância da classe para chamar os métodos dela e verificar o estado.
- No corpo da classe
GameViewModelTest
, declare uma propriedadeviewModel
e atribua uma instância da classeGameViewModel
a ela.
class GameViewModelTest {
private val viewModel = GameViewModel()
}
- Para criar um teste de unidade para o caminho de sucesso, crie uma função
gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset()
e inclua uma anotação@Test
nela.
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
}
}
- Importe o seguinte:
import org.junit.Test
Para transmitir uma palavra correta do jogador ao método viewModel.updateUserGuess()
, é necessário extrair a palavra correta desembaralhada da palavra embaralhada em GameUiState
. Para fazer isso, primeiro acesse o estado atual da IU do jogo.
- No corpo da função, crie uma variável
currentGameUiState
e atribuaviewModel.uiState.value
a ela.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
}
- Para descobrir o palpite correto do jogador, use a função
getUnscrambledWord()
, que usa ocurrentGameUiState.currentScrambledWord
como argumento e retorna a palavra desembaralhada. Armazene esse valor retornado em uma nova variável somente leitura chamadacorrectPlayerWord
e atribua o valor retornado pela funçãogetUnscrambledWord()
.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Para verificar se o palpite está correto, adicione uma chamada para o método
viewModel.updateUserGuess()
e transmita a variávelcorrectPlayerWord
como um argumento. Em seguida, adicione uma chamada para o métodoviewModel.checkUserGuess()
para verificar o palpite.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
}
Agora está tudo pronto para você declarar que o estado do jogo está como esperado.
- Acesse a instância da classe
GameUiState
com base no valor da propriedadeviewModel.uiState
e a armazene na variávelcurrentGameUiState
.
@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 conferir se o palpite está correto e a pontuação está atualizada, use
assertFalse()
para verificar se a propriedadecurrentGameUiState.isGuessedWordWrong
éfalse
eassertEquals()
para verificar se o valor da propriedadecurrentGameUiState.score
é 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)
}
- Importe o seguinte:
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
- Para tornar o valor
20
legível e reutilizável, crie um objeto complementar e atribua20
a uma constanteprivate
chamadaSCORE_AFTER_FIRST_CORRECT_ANSWER
. Atualize o teste com a constante recém-criada.
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
}
}
- Execute o teste.
O teste deve ser aprovado, já que todas as declarações eram válidas, como mostrado nesta captura de tela:
Caminho de erro
Para criar um teste de unidade para o caminho de erro, você precisa declarar que, quando uma palavra incorreta é transmitida como um argumento para o método viewModel.updateUserGuess()
e o método viewModel.checkUserGuess()
é chamado, acontece o seguinte:
- o valor da propriedade
currentGameUiState.score
permanece inalterado; - o valor da propriedade
currentGameUiState.isGuessedWordWrong
é definido comotrue
porque o palpite está errado.
Para criar o teste, siga estas etapas:
- No corpo da classe
GameViewModelTest
, crie uma funçãogameViewModel_IncorrectGuess_ErrorFlagSet()
e inclua a anotação@Test
nela.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
}
- Defina uma variável
incorrectPlayerWord
e atribua o valor"and"
a ela, que não pode existir na lista de palavras.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
}
- Adicione uma chamada ao método
viewModel.updateUserGuess()
e transmita a variávelincorrectPlayerWord
como um argumento. - Adicione uma chamada ao método
viewModel.checkUserGuess()
para verificar o palpite.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
}
- Adicione uma variável
currentGameUiState
e atribua o valor do estadoviewModel.uiState.value
a ela. - Use funções de declaração para declarar que o valor da propriedade
currentGameUiState.score
é0
e que o valor da propriedadecurrentGameUiState.isGuessedWordWrong
é definido comotrue
.
@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)
}
- Importe o seguinte:
import org.junit.Assert.assertTrue
- Execute o teste para confirmar se ele está aprovado.
Caso de limite
Para testar o estado inicial da interface, programe um teste de unidade para a classe GameViewModel
. O teste precisa declarar que, quando o GameViewModel
é inicializado, estas condições são verdadeiras:
- A propriedade
currentWordCount
está definida como1
. - A propriedade
score
está definida como0
. - A propriedade
isGuessedWordWrong
está definida comofalse
. - A propriedade
isGameOver
está definida comofalse
.
Siga as etapas abaixo para adicionar o teste:
- Crie um método
gameViewModel_Initialization_FirstWordLoaded()
e adicione a anotação@Test
a ele.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
}
- Acesse a propriedade
viewModel.uiState.value
para conseguir a instância inicial da classeGameUiState
. Atribua-o a uma nova variável somente leituragameUiState
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
}
- Para descobrir a palavra correta do jogador, use a função
getUnscrambledWord()
, que usa ogameUiState.currentScrambledWord
como palavra e retorna a palavra desembaralhada. Atribua o valor retornado a uma nova variável somente leitura chamadaunScrambledWord
.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
}
- Para verificar se o estado está correto, adicione as funções
assertTrue()
para declarar que a propriedadecurrentWordCount
está definida como1
e a propriedadescore
está definida como0
. - Adicione as funções
assertFalse()
para verificar se a propriedadeisGuessedWordWrong
éfalse
e se a propriedadeisGameOver
está definida 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)
}
- Importe o seguinte:
import org.junit.Assert.assertNotEquals
- Execute o teste para confirmar se ele está aprovado.
Outro caso de limite é testar o estado da IU depois que o usuário descobre todas as palavras. Você precisa declarar que, quando o usuário adivinha todas as palavras corretamente, estas condições são verdadeiras:
- O placar está atualizado.
- A propriedade
currentGameUiState.currentWordCount
é igual ao valor da constanteMAX_NO_OF_WORDS
. - A propriedade
currentGameUiState.isGameOver
está definida comotrue
.
Siga as etapas abaixo para adicionar o teste:
- Crie um método
gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly()
e adicione a anotação@Test
a ele. No método, crie uma variávelexpectedScore
e atribua0
a ela.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
}
- Para ver o estado inicial, adicione uma variável
currentGameUiState
e atribua o valor da propriedadeviewModel.uiState.value
a ela.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
}
- Para descobrir a palavra correta do jogador, use a função
getUnscrambledWord()
, que usa ocurrentGameUiState.currentScrambledWord
como palavra e retorna a palavra desembaralhada. Armazene esse valor retornado em uma nova variável somente leitura chamadacorrectPlayerWord
e atribua o valor retornado pela funçãogetUnscrambledWord()
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- Para testar se o usuário sabe todas as respostas, use um bloco
repeat
para repetir a execução do métodoviewModel.updateUserGuess()
eviewModel.checkUserGuess()
a mesma quantidade de vezes que oMAX_NO_OF_WORDS
.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
}
}
- No bloco
repeat
, adicione o valor da constanteSCORE_INCREASE
à variávelexpectedScore
para declarar que a pontuação aumenta após cada resposta correta. - Adicione uma chamada ao método
viewModel.updateUserGuess()
e transmita a variávelcorrectPlayerWord
como um argumento. - Adicione uma chamada ao método
viewModel.checkUserGuess()
para acionar a verificação do palpite do usuário.
@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()
}
}
- Atualize a palavra atual do jogador, use a função
getUnscrambledWord()
, que usa ocurrentGameUiState.currentScrambledWord
como um argumento e retorna a palavra desembaralhada. Armazene esse valor retornado em uma nova variável somente leitura com o nomecorrectPlayerWord.
. Para verificar se o estado está correto, adicione a funçãoassertEquals()
para conferir se o valor da propriedadecurrentGameUiState.score
é igual ao valor da variávelexpectedScore
.
@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)
}
}
- Adicione uma função
assertEquals()
para declarar que o valor da propriedadecurrentGameUiState.currentWordCount
é igual ao valor da constanteMAX_NO_OF_WORDS
e que o valor da propriedadecurrentGameUiState.isGameOver
é definido comotrue
.
@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)
}
- Importe o seguinte:
import com.example.unscramble.data.MAX_NO_OF_WORDS
- Execute o teste para confirmar se ele está aprovado.
Visão geral do ciclo de vida da instância de teste
Ao observar mais de perto a forma como o viewModel
é inicializado no teste, você pode perceber que o viewModel
é inicializado apenas uma vez, mesmo que todos os testes o usem. Este snippet de código mostra a definição da propriedade viewModel
.
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
...
}
...
}
Você pode ter as seguintes dúvidas:
- Isso significa que a mesma instância de
viewModel
é reutilizada para todos os testes? - Isso causa algum problema? E se o método de teste
gameViewModel_Initialization_FirstWordLoaded
for executado após o método de testegameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset
, por exemplo? O teste de inicialização vai falhar?
A resposta para essas perguntas é "não". Os métodos de teste são executados de forma isolada para evitar efeitos colaterais inesperados no estado mutável da instância de teste. Por padrão, antes de cada método de teste ser executado, o JUnit cria uma nova instância da classe de teste.
Como você tem quatro métodos de teste até o momento na classe GameViewModelTest
, a GameViewModelTest
é instanciada quatro vezes. Cada instância tem a própria cópia da propriedade viewModel
. Portanto, a sequência da execução do teste não importa.
5. Introdução à cobertura de código
A cobertura do código tem um papel vital para determinar se você testa adequadamente as classes, os métodos e as linhas de código que compõem o app.
O Android Studio oferece uma ferramenta de cobertura para testes de unidade locais a fim de acompanhar a porcentagem e as áreas do código do app cobertas por esses testes.
Executar testes com cobertura usando o Android Studio
Para executar testes com cobertura:
- Clique com o botão direito do mouse no arquivo
GameViewModelTest.kt
no painel do projeto e selecione a opção Run 'GameViewModelTest' with Coverage.
- Após a conclusão da execução do teste, no painel de cobertura à direita, clique na opção Flatten Packages.
- Observe o pacote
com.example.android.unscramble.ui
como mostrado na imagem abaixo.
- Clique duas vezes no nome do pacote
com.example.android.unscramble.ui
, isso mostra a cobertura paraGameViewModel
, como na imagem abaixo:
Analisar relatórios de teste
O relatório mostrado no diagrama a seguir é dividido em dois aspectos:
- Porcentagem de métodos cobertos pelos testes de unidade: no diagrama de exemplo, os testes que você criou até agora cobriram sete dos oito métodos. Isso representa 87% do total de métodos.
- Porcentagem de linhas cobertas pelos testes de unidade: no diagrama de exemplo, os testes que você criou cobriram 39 das 41 linhas de código. Isso representa 95% das linhas de código.
Os relatórios sugerem que os testes de unidade que você criou até agora perderam determinadas partes do código. Para determinar quais partes estão faltando, siga esta etapa:
- Clique duas vezes em GameViewModel.
O Android Studio mostra o arquivo GameViewModel.kt
com as cores da programação no lado esquerdo da janela. A cor verde indica que essas linhas de código foram cobertas.
Ao rolar a tela para baixo no GameViewModel
, algumas linhas serão marcadas com a cor rosa-claro. Essa cor indica que essas linhas de código não foram cobertas pelos testes de unidade.
Melhorar a cobertura
Para melhorar a cobertura, é necessário criar um teste que cubra o caminho ausente. Você precisa adicionar um teste para declarar que, quando um usuário pula uma palavra, as seguintes condições são verdadeiras:
- A propriedade
currentGameUiState.score
permanece inalterada. - A propriedade
currentGameUiState.currentWordCount
é aumentada em um, conforme mostrado no snippet de código abaixo.
Para se preparar para melhorar a cobertura, adicione o seguinte método de teste à 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)
}
Para executar a cobertura novamente, siga estas etapas:
- Clique com o botão direito do mouse no arquivo
GameViewModelTest.kt
e no menu e selecione Run 'GameViewModelTest' with Coverage. - Após a criação, acesse o elemento GameViewModel novamente e confirme se a porcentagem da cobertura é de 100%. O relatório final da cobertura é mostrado na imagem abaixo.
- Navegue até o arquivo
GameViewModel.kt
e role para baixo para conferir se o caminho que foi perdido anteriormente está coberto.
Você aprendeu a executar, analisar e melhorar a cobertura do código do aplicativo.
Uma alta porcentagem de cobertura de código significa que o código do app tem alta qualidade? Não. A cobertura do código indica a porcentagem do código coberto ou executado pelo teste de unidade. Isso não indica que o código foi verificado. Se você remover todas as declarações do código de teste de unidade e executar a cobertura do código, ele ainda vai mostrar 100% de cobertura.
Uma alta cobertura não indica que os testes foram criados corretamente e que os testes confirmam o comportamento do app. Os testes que você criou precisam ter as declarações que confirmam o comportamento da classe que está sendo testada. Também não é necessário programar testes de unidade para receber uma cobertura de 100% para todo o app. Teste algumas partes do código, como Atividades, usando testes de interface.
No entanto, uma cobertura baixa significa que partes grandes do seu código não foram totalmente testadas. Use a cobertura do código como uma ferramenta para encontrar as partes do código que não foram executadas pelos testes, em vez de uma ferramenta para medir a qualidade dele.
6. Acessar o código da solução
Para baixar o código do codelab concluído, use estes comandos 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
Se preferir, você pode baixar o repositório como um arquivo ZIP, descompactar e abrir no Android Studio.
Se você quiser ver o código da solução, acesse o GitHub (link em inglês).
7. Conclusão
Parabéns! Você aprendeu a definir a estratégia de testes e implementar testes de unidade para avaliar o ViewModel
e StateFlow
no app Unscramble. Ao continuar criando apps Android, programe testes junto aos recursos do seu app para confirmar se eles funcionam corretamente durante todo o processo de desenvolvimento.
Resumo
- Use a configuração
testImplementation
para indicar que as dependências se aplicam ao código-fonte do teste local, e não ao código do aplicativo. - Tente categorizar testes em três cenários: caminho de sucesso, caminho de erro e caso de limite.
- Um bom teste de unidade tem pelo menos quatro características: é focado, compreensível, determinístico e autônomo.
- Os métodos de teste são executados de forma isolada para evitar efeitos colaterais inesperados no estado mutável da instância de teste.
- Por padrão, antes de cada método de teste ser executado, o JUnit cria uma nova instância da classe de teste.
- A cobertura do código tem um papel vital para determinar se as classes, os métodos e as linhas de código que compõem o app foram testadas adequadamente.