Criar testes de unidade para ViewModel

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:

bb1e97c357603a27.png

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

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:

Pasta de teste no painel do projeto do Android Studio

O código inicial tem este arquivo:

  • WordsData.kt: contém uma lista de palavras a serem usadas para testes e uma função auxiliar getUnscrambledWord() 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:

  1. Abra o arquivo build.gradle.kts do módulo app, localizado no diretório app no painel Project.

Arquivo build.gradle.kts no painel do projeto

  1. Dentro do arquivo, role a tela para baixo até encontrar o bloco dependencies{}. Adicione uma dependência usando a configuração testImplementation para junit.
plugins {
    ...
}

android {
    ...
}

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

1c20fc10750ca60c.png

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 e isGuessedWordWrong é atualizado corretamente.

Para criar o teste, siga estas etapas:

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

57d004ccc4d75833.png

f98067499852bdce.png

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.

  1. No corpo da classe GameViewModelTest, declare uma propriedade viewModel e atribua uma instância da classe GameViewModel a ela.
class GameViewModelTest {
    private val viewModel = GameViewModel()
}
  1. 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()  {
    }
}
  1. 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.

  1. No corpo da função, crie uma variável currentGameUiState e atribua viewModel.uiState.value a ela.
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
}
  1. Para descobrir o palpite correto do jogador, use a função getUnscrambledWord(), que usa o currentGameUiState.currentScrambledWord como argumento e retorna a palavra desembaralhada. Armazene esse valor retornado em uma nova variável somente leitura chamada unScrambledWord e atribua o valor retornado pela função getUnscrambledWord().
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
    var currentGameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)

}
  1. Para verificar se o palpite está correto, adicione uma chamada para o método viewModel.updateUserGuess() e transmita a variável correctPlayerWord como um argumento. Em seguida, adicione uma chamada para o método viewModel.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.

  1. Acesse a instância da classe GameUiState com base no valor da propriedade viewModel.uiState e a armazene na variável 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 conferir se o palpite está correto e a pontuação está atualizada, use assertFalse() para verificar se a propriedade currentGameUiState.isGuessedWordWrong é false e assertEquals() para verificar se o valor da propriedade currentGameUiState.score é 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. Importe o seguinte:
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
  1. Para tornar o valor 20 legível e reutilizável, crie um objeto complementar e atribua 20 a uma constante private chamada SCORE_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
    }
}
  1. Execute o teste.

O teste deve ser aprovado, já que todas as declarações eram válidas, como mostrado nesta captura de tela:

c6bd246467737a32.png

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 como true porque o palpite está errado.

Para criar o teste, siga estas etapas:

  1. No corpo da classe GameViewModelTest, crie uma função gameViewModel_IncorrectGuess_ErrorFlagSet() e inclua a anotação @Test nela.
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {

}
  1. 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"
}
  1. Adicione uma chamada ao método viewModel.updateUserGuess() e transmita a variável incorrectPlayerWord como um argumento.
  2. 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()
}
  1. Adicione uma variável currentGameUiState e atribua o valor do estado viewModel.uiState.value a ela.
  2. Use funções de declaração para declarar que o valor da propriedade currentGameUiState.score é 0 e que o valor da propriedade currentGameUiState.isGuessedWordWrong é definido como 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. Importe o seguinte:
import org.junit.Assert.assertTrue
  1. 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 como 1.
  • A propriedade score está definida como 0.
  • A propriedade isGuessedWordWrong está definida como false.
  • A propriedade isGameOver está definida como false.

Siga as etapas abaixo para adicionar o teste:

  1. Crie um método gameViewModel_Initialization_FirstWordLoaded() e adicione a anotação @Test a ele.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {

}
  1. Acesse a propriedade viewModel.uiState.value para conseguir a instância inicial da classe GameUiState. Atribua-o a uma nova variável somente leitura gameUiState.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
}
  1. Para descobrir a palavra correta do jogador, use a função getUnscrambledWord(), que usa o gameUiState.currentScrambledWord como palavra e retorna a palavra desembaralhada. Atribua o valor retornado a uma nova variável somente leitura chamada unScrambledWord.
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
    val gameUiState = viewModel.uiState.value
    val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)

}
  1. Para verificar se o estado está correto, adicione as funções assertTrue() para declarar que a propriedade currentWordCount está definida como 1 e a propriedade score está definida como 0.
  2. Adicione as funções assertFalse() para verificar se a propriedade isGuessedWordWrong é false e se a propriedade isGameOver está definida 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. Importe o seguinte:
import org.junit.Assert.assertNotEquals
  1. 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 constante MAX_NO_OF_WORDS.
  • A propriedade currentGameUiState.isGameOver está definida como true.

Siga as etapas abaixo para adicionar o teste:

  1. Crie um método gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() e adicione a anotação @Test a ele. No método, crie uma variável expectedScore e atribua 0 a ela.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
}
  1. Para ver o estado inicial, adicione uma variável currentGameUiState e atribua o valor da propriedade viewModel.uiState.value a ela.
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
}
  1. Para descobrir a palavra correta do jogador, use a função getUnscrambledWord(), que usa o currentGameUiState.currentScrambledWord como palavra e retorna a palavra desembaralhada. Armazene esse valor retornado em uma nova variável somente leitura chamada correctPlayerWord e atribua o valor retornado pela função getUnscrambledWord().
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
    var expectedScore = 0
    var currentGameUiState = viewModel.uiState.value
    var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
  1. Para testar se o usuário sabe todas as respostas, use um bloco repeat para repetir a execução do método viewModel.updateUserGuess() e viewModel.checkUserGuess() a mesma quantidade de vezes que o MAX_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) {

    }
}
  1. No bloco repeat, adicione o valor da constante SCORE_INCREASE à variável expectedScore para declarar que a pontuação aumenta após cada resposta correta.
  2. Adicione uma chamada ao método viewModel.updateUserGuess() e transmita a variável correctPlayerWord como um argumento.
  3. 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()
    }
}
  1. Atualize a palavra atual do jogador, use a função getUnscrambledWord(), que usa o currentGameUiState.currentScrambledWord como um argumento e retorna a palavra desembaralhada. Armazene esse valor retornado em uma nova variável somente leitura com o nome correctPlayerWord.. Para verificar se o estado está correto, adicione a função assertEquals() para conferir se o valor da propriedade currentGameUiState.score é igual ao valor da variável 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. Adicione uma função assertEquals() para declarar que o valor da propriedade currentGameUiState.currentWordCount é igual ao valor da constante MAX_NO_OF_WORDS e que o valor da propriedade currentGameUiState.isGameOver é definido como 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. Importe o seguinte:
import com.example.unscramble.data.MAX_NO_OF_WORDS
  1. 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 teste gameViewModel_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:

  1. Clique com o botão direito do mouse no arquivo GameViewModelTest.kt no painel do projeto e selecione a opção 28f58fea5649f4d5.png Run 'GameViewModelTest' with Coverage.

Painel do projeto com a opção de executar teste do modelo de visualização de jogos com a opção de cobertura selecionada

  1. Após a conclusão da execução do teste, no painel de cobertura à direita, clique na opção Flatten Packages.

A opção "Flatten packages" está destacada

  1. Observe o pacote com.example.android.unscramble.ui como mostrado na imagem abaixo.

a4408d8870366144.png

  1. Clique duas vezes no nome do pacote com.example.android.unscramble.ui e mostre a cobertura para GameViewModel, como na imagem abaixo:

3ec7ea7896b52f3a.png

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.

d78155448e2b9304.png

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.

9348d72ff2737009.png

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.

dd2419cd8af3a486.png

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:

  1. Clique com o botão direito do mouse no arquivo GameViewModelTest.kt e no menu e selecione 28f58fea5649f4d5.png Run 'GameViewModelTest' with Coverage.
  2. Para recompilar os resultados após a nova execução, clique no botão Recompile quando um prompt aparecer, conforme mostrado na imagem:

4b938d2efe289fbc.png

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

e91469b284854b8c.png

  1. Navegue até o arquivo GameViewModel.kt e role para baixo para conferir se o caminho que foi perdido anteriormente está coberto.

5b96c0b7300e6f06.png

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 fazer o download do 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 fazer o download do 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.

Saiba mais