Princípios básicos dos testes

Este codelab faz parte do curso Android avançado no Kotlin. Você aproveitará mais o curso se fizer os codelabs em sequência, mas isso não é obrigatório. Todos os codelabs do curso estão listados na página de destino dos codelabs avançados do Android em Kotlin (link em inglês).

Introdução

Quando você implementou o primeiro recurso do primeiro app, é provável que tenha executado o código para verificar se ele funcionou conforme o esperado. Você fez um teste, mesmo que seja um teste manual. À medida que você continuou a adicionar e atualizar recursos, provavelmente também continuou a executar seu código e verificar se ele funciona. Mas o processo manual é cansativo, propenso a erros e não escalona.

Os computadores são ótimos para escalonamento e automação. Portanto, os desenvolvedores de grandes e pequenas empresas criam testes automatizados, que são executados por software e não exigem que você opere manualmente o app para verificar se o código funciona.

Nesta série de codelabs, você aprenderá a criar um conjunto de testes (conhecido como conjunto de testes) para um app real.

Este primeiro codelab aborda os conceitos básicos de testes no Android. Você criará seus primeiros testes e aprenderá a testar LiveData e ViewModels.

O que você já precisa saber

Você precisa:

O que você vai aprender

Você aprenderá sobre os seguintes tópicos:

  • Como criar e executar testes de unidade no Android
  • Como usar o desenvolvimento orientado a testes
  • Como escolher testes instrumentados e locais

Você aprenderá sobre as seguintes bibliotecas e conceitos de código:

Atividades do laboratório

  • Configurar, executar e interpretar testes locais e instrumentados no Android.
  • Crie testes de unidade no Android usando JUnit4 e Hamcrest.
  • Crie testes LiveData e ViewModel simples.

Nesta série de codelabs, você trabalhará com o app TO-DO Notes. Com ele, você pode anotar as tarefas a serem concluídas e exibi-las em uma lista. Você pode marcá-los como concluídos ou não, filtrá-los ou excluí-los.

Este app é escrito em Kotlin, tem várias telas, usa componentes do Jetpack e segue a arquitetura de um Guia para a arquitetura do app. Ao aprender a testar esse app, você poderá testar apps que usam as mesmas bibliotecas e arquiteturas.

Para começar, faça o download do código:

Fazer o download do ZIP

Como alternativa, é possível clonar o repositório do GitHub para o código:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout starter_code

Nesta tarefa, você executará o app e explorará a base do código.

Etapa 1: executar o app de exemplo

Depois de fazer o download do app de tarefas, abra-o no Android Studio e execute-o. Ele será compilado. Explore o aplicativo fazendo o seguinte:

  • Crie uma nova tarefa com o botão de ação flutuante flutuante. Digite um título primeiro e, em seguida, informações adicionais sobre a tarefa. Salve-a com o FAB de verificação verde.
  • Na lista de tarefas, clique no título da tarefa que você acabou de concluir e observe a tela de detalhes dela para ver o restante da descrição.
  • Na lista ou na tela de detalhes, marque a caixa de seleção da tarefa para definir o status como Concluída.
  • Volte para a tela "Tarefas", abra o menu e filtre as tarefas pelo status Ativo e Concluído.
  • Abra a gaveta de navegação e clique em Estatísticas.
  • Volte para a tela "Visão geral". No menu da gaveta de navegação, selecione Limpar concluídas para excluir todas as tarefas com o status Concluída.

Etapa 2: analisar o exemplo de código do app

O app de tarefas é baseado no conhecido exemplo de arquitetura e teste Architecture Blueprints, que usa a versão de arquitetura reativa do exemplo. O app segue a arquitetura de um Guia para a arquitetura do app. Ele usa ViewModels com fragmentos, um repositório e Room. Se você conhece algum dos exemplos abaixo, esse app tem uma arquitetura semelhante:

É mais importante entender a arquitetura geral do app do que ter um profundo conhecimento da lógica em qualquer camada.

Veja o resumo dos pacotes que você encontrará:

Pacote: com.example.android.architecture.blueprints.todoapp

.addedittask

Adicionar ou editar uma tela de tarefas: código da camada de IU para adicionar ou editar uma tarefa.

.data

Camada de dados: lida com a camada de dados das tarefas. Ele contém o código do banco de dados, da rede e do repositório.

.statistics

Tela de estatísticas: código da camada de IU para a tela de estatísticas.

.taskdetail

Tela de detalhes da tarefa:código da camada de IU para uma única tarefa.

.tasks

Tela de tarefas: código da camada de IU para a lista de todas as tarefas.

.util

Classes de utilitários: classes compartilhadas usadas em várias partes do app, por exemplo, para o layout de atualização de deslize usado em várias telas.

Camada de dados (.data)

Esse app inclui uma camada de rede simulada no pacote remoto e uma camada de banco de dados no pacote local. Para simplificar, neste projeto a camada de rede é simulada com apenas um HashMap com atraso, em vez de fazer solicitações de rede reais.

As coordenadas de DefaultTasksRepository ou mediam entre a camada de rede e a de banco de dados. É isso que retorna os dados para a camada de IU.

Camada de IU ( .addedittask, .statistics, .taskdetail, .tasks)

Cada um dos pacotes de camada de IU contém um fragmento e um modelo de visualização, além de outras classes necessárias para a IU (como um adaptador para a lista de tarefas). O TaskActivity é a atividade que contém todos os fragmentos.

Navegação

A navegação do app é controlada pelo componente de navegação. Ela é definida no arquivo nav_graph.xml. A navegação é acionada nos modelos de visualização usando a classe Event. Os modelos de visualização também determinam quais argumentos transmitir. Os fragmentos observam as Events e fazem a navegação real entre as telas.

Nesta tarefa, você vai executar seus primeiros testes.

  1. No Android Studio, abra o painel Project e encontre estas três pastas:
  • com.example.android.architecture.blueprints.todoapp
  • com.example.android.architecture.blueprints.todoapp (androidTest)
  • com.example.android.architecture.blueprints.todoapp (test)

Essas pastas são conhecidas como conjuntos de origem. Conjuntos de origem são pastas que contêm o código-fonte do seu app. Os conjuntos de origem coloridos em verde (androidTest e test) contêm os testes. Ao criar um novo projeto Android, você recebe os três conjuntos de origem a seguir por padrão. São eles:

  • main: contém o código do app. Esse código é compartilhado entre todas as diferentes versões do app que você pode criar (conhecidas como variantes de compilação).
  • androidTest: contém testes conhecidos como testes de instrumentação.
  • test: contém testes conhecidos como testes locais.

A diferença entre testes locais e testes de instrumentação está na forma como eles são executados.

Testes locais (test conjunto de origem)

Esses testes são executados localmente na JVM da máquina de desenvolvimento e não exigem um emulador ou dispositivo físico. Por isso, elas correm rapidamente, mas a fidelidade é menor, o que significa que elas funcionam menos como no mundo real.

No Android Studio, os testes locais são representados por um ícone de triângulo verde e vermelho.

Testes de instrumentação (androidTest conjunto de origem)

Esses testes são executados em dispositivos Android reais ou emulados e refletem o que acontecerá no mundo real, mas também serão muito mais lentos.

No Android Studio, os testes de instrumentação são representados por um Android com um ícone de triângulo verde e vermelho.

Etapa 1: executar um teste local

  1. Abra a pasta test até encontrar o arquivo ExampleUnitTest.kt.
  2. Clique com o botão direito nele e selecione Run ExampleUnitTest.

Você verá a seguinte saída na janela Run na parte inferior da tela:

  1. Observe as marcas de seleção verdes e expanda os resultados do teste para confirmar que um teste chamado addition_isCorrect foi aprovado. É bom saber que a adição funciona como esperado.

Etapa 2: fazer o teste falhar

Veja abaixo o teste que você acabou de executar.

ExampleUnitTest.kt (link em inglês)

// A test class is just a normal class
class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       // Here you are checking that 4 is the same as 2+2
       assertEquals(4, 2 + 2)
   }
}

Os testes

  • são uma classe em um dos conjuntos de origem de teste.
  • contêm funções que começam com a anotação @Test (cada função é um único teste).
  • u8sually contém declarações de declaração.

O Android usa a biblioteca JUnit para realizar testes (neste codelab JUnit4). As declarações e a anotação @Test vêm do JUnit.

Uma declaração é o núcleo do teste. Essa é uma instrução de código que verifica se o código ou o app se comportam conforme o esperado. Nesse caso, a declaração é assertEquals(4, 2 + 2), que verifica que 4 é igual a 2 + 2.

Para ver a aparência de um teste com falha, adicione uma declaração que pode ser vista com facilidade. Verificará 3 se for igual a 1+1.

  1. Adicione assertEquals(3, 1 + 1) ao teste addition_isCorrect.

ExampleUnitTest.kt (link em inglês)

class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
       assertEquals(3, 1 + 1) // This should fail
   }
}
  1. Execute o teste.
  1. Nos resultados, observe um X ao lado do teste.

  1. Observe também o seguinte:
  • Uma única declaração com falha falhará em todo o teste.
  • Você verá o valor esperado (3) em relação ao valor que foi realmente calculado (2).
  • Você será direcionado para a linha da declaração (ExampleUnitTest.kt:16) reprovada.

Etapa 3: executar um teste de instrumentação

Os testes de instrumentação estão no conjunto de origem androidTest.

  1. Abra o conjunto de origem androidTest.
  2. Execute o teste chamado ExampleInstrumentedTest.

ExampleInstrumentedTest (link em inglês)

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.android.architecture.blueprints.reactive",
            appContext.packageName)
    }
}

Ao contrário do teste local, esse teste é executado em um dispositivo (no exemplo abaixo de um smartphone Pixel 2 emulado):

Se você tiver um dispositivo conectado ou um emulador em execução, verá o teste ser executado no emulador.

Nesta tarefa, você vai criar testes para o getActiveAndCompleteStats, que calcula a porcentagem das estatísticas de tarefas ativas e completas do app. É possível ver esses números na tela de estatísticas do app.

Etapa 1: criar uma classe de teste

  1. No main conjunto de origem, abra todoapp.statistics e abra StatisticsUtils.kt.
  2. Localize a função getActiveAndCompletedStats.

StatisticsUtils.kt (link em inglês)

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)

A função getActiveAndCompletedStats aceita uma lista de tarefas e retorna um StatsResult. StatsResult é uma classe de dados que contém dois números: a porcentagem de tarefas concluídas e a porcentagem que estão ativas.

O Android Studio oferece ferramentas para gerar stubs de teste e ajudar a implementar os testes para essa função.

  1. Clique com o botão direito do mouse em getActiveAndCompletedStats e selecione Gerar > Testar.

A caixa de diálogo Create Test será aberta:

  1. Mude o Class name: para StatisticsUtilsTest (em vez de StatisticsUtilsKtTest; é um pouco mais agradável não usar KT no nome da classe de teste).
  2. Mantenha o restante dos padrões. O JUnit 4 é a biblioteca de testes apropriada. O pacote de destino está correto (espelha a localização da classe StatisticsUtils) e você não precisa marcar nenhuma das caixas de seleção. Isso gera um código extra, mas você criará seu teste do zero.
  3. Pressione OK.

A caixa de diálogo Choose Destination Directory é aberta:

Você fará um teste local porque a função está fazendo cálculos matemáticos e não incluirá nenhum código específico do Android. Portanto, não é necessário executá-lo em um dispositivo real ou emulado.

  1. Selecione o diretório test (não androidTest) porque você criará testes locais.
  2. Clique em OK.
  3. Observe que a classe StatisticsUtilsTest gerou em test/statistics/.

Etapa 2: criar a primeira função de teste

Você criará um teste que verifica:

  • se não houver tarefas concluídas e uma tarefa ativa,
  • que a porcentagem de testes ativos é de 100%,
  • e a porcentagem de tarefas concluídas é de 0%.
  1. Abra o StatisticsUtilsTest
  2. Crie uma função chamada getActiveAndCompletedStats_noCompleted_returnsHundredZero.

StatisticsUtilsTest.kt (link em inglês)

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. Adicione a anotação @Test acima do nome da função para indicar que ela é um teste.
  2. Crie uma lista de tarefas.
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. Chame getActiveAndCompletedStats com essas tarefas.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. Verifique se result é o esperado, usando declarações.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

Veja o código completo.

StatisticsUtilsTest.kt (link em inglês)

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active task (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertEquals(result.completedTasksPercent, 0f)
        assertEquals(result.activeTasksPercent, 100f)
    }
}
  1. Execute o teste (clique com o botão direito do mouse em StatisticsUtilsTest e selecione Run).

Ele será aprovado em:

Etapa 3: adicionar a dependência do Hamcrest

Como os testes funcionam como uma documentação do que o código faz, é bom quando eles são legíveis. Compare as duas declarações a seguir:

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

A segunda declaração é muito mais que uma frase humana. Ela foi escrita usando um framework de declaração chamado Hamcrest. Outra boa ferramenta para criar declarações legíveis é a Truth Library. Neste codelab, você usará o Hamcrest para escrever declarações.

  1. Abra build.grade (Module: app) e adicione a seguinte dependência.

app/build.gradle

dependencies {
    // Other dependencies
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

Normalmente, você usa implementation ao adicionar uma dependência, mas aqui está o testImplementation. Quando estiver tudo pronto para compartilhar seu app com o mundo, é recomendável não sobrecarregar o tamanho do APK com os códigos ou as dependências do app. É possível designar se uma biblioteca deve ser incluída no código principal ou de teste usando as configurações do Gradle. As configurações mais comuns são as seguintes:

  • implementation: a dependência está disponível em todos os conjuntos de origem, incluindo os conjuntos de origem de teste.
  • testImplementation: a dependência está disponível apenas no conjunto de origem de teste.
  • androidTestImplementation: a dependência está disponível apenas no conjunto de origem androidTest.

Qual configuração você usa define onde a dependência pode ser usada. Se você escrever:

testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

Isso significa que o Hamcrest só estará disponível no conjunto de origem de teste. Ele também garante que o Hamcrest não seja incluído no app final.

Etapa 4: usar o Hamcrest para escrever declarações

  1. Atualize o teste getActiveAndCompletedStats_noCompleted_returnsHundredZero() para usar assertThat do Hamcrest em vez de assertEquals.
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))

É possível usar a importação import org.hamcrest.Matchers.`is` se solicitado.

O teste final ficará parecido com o código abaixo.

StatisticsUtilsTest.kt (link em inglês)

import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {

        // Create an active tasks (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))

    }
}
  1. Execute o teste atualizado para confirmar se ele ainda funciona.

Este codelab não ensinará todos os detalhes do Hamcrest. Se quiser saber mais, confira o tutorial oficial.

Esta é uma tarefa opcional para praticar.

Nesta tarefa, você vai criar mais testes usando JUnit e Hamcrest. Você também gravará testes usando uma estratégia derivada da prática do programa de Desenvolvimento voltado para testes. Desenvolvimento voltado para testes, ou TDD, é uma escola de pensamento de programação que diz que, em vez de escrever o código do recurso primeiro, os testes são criados primeiro. Em seguida, programe o código de recurso com o objetivo de passar nos testes.

Etapa 1. Criar testes

Escreva testes para quando você tiver uma lista de tarefas normal:

  1. Se houver uma tarefa concluída e nenhuma tarefa ativa, a porcentagem de activeTasks será 0f, e a porcentagem de tarefas concluídas precisará ser 100f .
  2. Se houver duas tarefas concluídas e três tarefas ativas, a porcentagem concluída precisa ser 40f, e a porcentagem ativa precisa ser 60f.

Etapa 2: Criar um teste para um bug

O código para o getActiveAndCompletedStats conforme escrito tem um bug. Repare como ela não processa corretamente o que acontece quando a lista está vazia ou é nula. Nos dois casos, as duas porcentagens devem ser zero.

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

Para corrigir o código e programar testes, você usará o desenvolvimento orientado a testes. O teste baseado em testes segue estas etapas.

  1. Escreva o teste usando a estrutura especificada, quando e depois e com um nome que siga a convenção.
  2. Confirme se o teste falha.
  3. Escreva o código mínimo para passar no teste.
  4. Repita o procedimento para todos os testes.

Em vez de começar a corrigir o bug, primeiro você precisa escrever os testes primeiro. Você pode confirmar se tem testes protegendo você de reintroduzir acidentalmente esses bugs no futuro.

  1. Se houver uma lista vazia (emptyList()), ambas as porcentagens deverão ser 0f.
  2. Se ocorrer um erro ao carregar as tarefas, a lista será null e as duas porcentagens serão 0f.
  3. Execute os testes e confirme se eles falham:

Etapa 3. Corrigir o erro

Agora que você já tem os testes, corrija o bug.

  1. Corrija o bug no getActiveAndCompletedStats retornando 0f se tasks for null ou estiver vazio:
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}
  1. Execute seus testes novamente e confirme se todos eles foram aprovados.

Ao seguir o TDD e criar os testes primeiro, você ajudou a garantir que:

  • Os novos recursos sempre têm testes associados. Assim, eles funcionam como uma documentação da função do código.
  • Seus testes verificam os resultados corretos e protegem contra bugs que você já viu.

Solução: criar mais testes

Veja todos os testes e o código de recurso correspondente.

StatisticsUtilsTest.kt (link em inglês)

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
        val tasks = listOf(
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed with an active task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 100 and 0
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When the list of tasks is computed with a completed task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 0 and 100
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(100f))
    }

    @Test
    fun getActiveAndCompletedStats_both_returnsFortySixty() {
        // Given 3 completed tasks and 2 active tasks
        val tasks = listOf(
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = false),
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed
        val result = getActiveAndCompletedStats(tasks)

        // Then the result is 40-60
        assertThat(result.activeTasksPercent, `is`(40f))
        assertThat(result.completedTasksPercent, `is`(60f))
    }

    @Test
    fun getActiveAndCompletedStats_error_returnsZeros() {
        // When there's an error loading stats
        val result = getActiveAndCompletedStats(null)

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_empty_returnsZeros() {
        // When there are no tasks
        val result = getActiveAndCompletedStats(emptyList())

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }
}

StatisticsUtils.kt (link em inglês)

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}

Bom trabalho com as noções básicas de como criar e executar testes. A seguir, você aprenderá a programar testes básicos do ViewModel e do LiveData.

No restante do codelab, você aprenderá a programar testes para duas classes do Android comuns na maioria dos apps: ViewModel e LiveData.

Comece escrevendo testes para o TasksViewModel.


Você se concentrará em testes que têm toda a lógica do modelo de visualização e não dependem do código do repositório. O código do repositório envolve código assíncrono, bancos de dados e chamadas de rede, que aumentam a complexidade do teste. Por enquanto, você vai evitar isso e se concentrar em escrever testes para a funcionalidade do ViewModel que não testam diretamente nada no repositório.



O teste que você criará verificará se você chama o método addNewTask para abrir a Event abertura da nova janela de tarefas. Veja o código do app que você vai testar.

TasksViewModel.kt.

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

Etapa 1. Criar uma classe TasksViewModelTest

Seguindo as mesmas etapas realizadas para StatisticsUtilTest, nesta etapa você criará um arquivo de teste para TasksViewModelTest.

  1. Abra a classe que você quer testar, no pacote tasks, TasksViewModel..
  2. No código, clique com o botão direito do mouse no nome da classe TasksViewModel -> Generate -> Test.

  1. Na tela Create Test, clique em OK para aceitar. Não é necessário alterar as configurações padrão.
  2. Na caixa de diálogo Choose Destination Directory, escolha o diretório test.

Etapa 2: Começar a programar seu teste do ViewModel

Nesta etapa, você adicionará um teste de modelo de visualização para testar se, ao chamar o método addNewTask, o Event para abrir a nova janela de tarefas é acionado.

  1. Crie um novo teste com o nome addNewTask_setsNewTaskEvent.

TasksViewModelTest.kt (link em inglês)

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

E o contexto do aplicativo?

Quando você cria uma instância de TasksViewModel para testar, o construtor exige um Application Context. Mas neste teste, você não está criando um aplicativo completo com atividades e IU e fragmentos, então como ter acesso a um contexto do aplicativo?

TasksViewModelTest.kt (link em inglês)

// Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(???)

As bibliotecas de teste do AndroidX incluem classes e métodos que oferecem versões dos componentes, como aplicativos e atividades, destinados a testes. Quando você tiver um teste local em que precise de classes de framework Android simuladas (como um contexto de aplicativo), siga estas etapas para configurar corretamente o AndroidX Test:

  1. Adicione as dependências principais e extremas do AndroidX Test.
  2. Adicionar a dependência da biblioteca Robolectric Testing
  3. Anotar a classe com o executor de testes do AndroidJunit4
  4. Criar código do AndroidX Test

Você concluirá essas etapas e então entenderá o que elas fazem juntas.

Etapa 3. Adicionar as dependências do Gradle

  1. Copie essas dependências no arquivo build.gradle do módulo do app para adicionar as dependências principais e externas do AndroidX Test, bem como a dependência de teste Robolectric.

app/build.gradle

    // AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"

    testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"

 testImplementation "org.robolectric:robolectric:$robolectricVersion"

Etapa 4. Adicionar Executor de teste do JUnit

  1. Adicione @RunWith(AndroidJUnit4::class) acima da classe de teste.

TasksViewModelTest.kt (link em inglês)

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

Etapa 5: Usar o AndroidX Test

Agora, você pode usar a biblioteca de testes AndroidX. Isso inclui o método ApplicationProvider.getApplicationContext, que recebe um contexto de aplicativo.

  1. Crie um TasksViewModel usando ApplicationProvider.getApplicationContext() da biblioteca de teste do AndroidX.

TasksViewModelTest.kt (link em inglês)

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. Ligar para addNewTask no tasksViewModel.

TasksViewModelTest.kt (link em inglês)

tasksViewModel.addNewTask()

Seu teste ficará parecido com o do código abaixo.

TasksViewModelTest.kt (link em inglês)

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
  1. Execute seu teste para confirmar se ele funciona.

Conceito: como o AndroidX Test funciona?

O que é o AndroidX Test?

O AndroidX Test é uma coleção de bibliotecas para teste. Ele inclui classes e métodos que fornecem versões de componentes, como aplicativos e atividades, destinados a testes. Como exemplo, o código que você escreveu é um exemplo de uma função do AndroidX Test para receber um contexto do aplicativo.

ApplicationProvider.getApplicationContext()

Um dos benefícios das APIs de teste do AndroidX é que elas são criadas para funcionar com testes locais e instrumentados. Isso é bom porque:

  • É possível fazer o mesmo teste local ou instrumentado.
  • Não é preciso aprender APIs de teste diferentes para testes locais e instrumentados.

Por exemplo, como você escreveu o código usando bibliotecas de teste do AndroidX, pode mover a classe TasksViewModelTest da pasta test para a pasta androidTest. Assim, os testes ainda serão executados. O getApplicationContext() funciona de maneira um pouco diferente dependendo de ele estar sendo executado como um teste local ou instrumentado:

  • Se for um teste instrumentado, ele receberá o contexto real do app fornecido ao inicializar um emulador ou se conectar a um dispositivo real.
  • Se for um teste local, ele usará um ambiente simulado do Android.

O que é o Robolectric?

O ambiente simulado do Android que o AndroidX Test usa para testes locais é fornecido pelo Robolectric. A biblioteca Robolectric (link em inglês) cria um ambiente simulado do Android para testes e é executada mais rapidamente do que inicializar um emulador ou executar um dispositivo. Sem a dependência do Robolectric, você receberá este erro:

O que @RunWith(AndroidJUnit4::class) faz?

Um executor de testes é um componente JUnit que executa testes. Sem um executor, seus testes não seriam executados. Há um executor de testes padrão fornecido pelo JUnit que você recebe automaticamente. @RunWith substitui esse executor de testes padrão.

O executor de testes AndroidJUnit4 permite que o AndroidX Test execute o teste de maneiras diferentes, dependendo se são testes instrumentados ou locais.

Etapa 6: Corrigir avisos robolétricos

Quando você executar o código, observe que o Robolectric é usado.

Graças ao AndroidX Test e ao executor de testes AndroidJunit4, isso é feito sem que você precise escrever uma única linha de código Robolectric diretamente.

Você pode ver dois avisos.

  • No such manifest file: ./AndroidManifest.xml
  • "WARN: Android SDK 29 requires Java 9..."

Para corrigir o aviso No such manifest file: ./AndroidManifest.xml, atualize o arquivo Gradle.

  1. Adicione a seguinte linha ao seu arquivo do Gradle para que o manifesto correto do Android seja usado. A opção includeAndroidResources permite acessar recursos do Android nos testes de unidade, incluindo o arquivo AndroidManifest.

app/build.gradle

    // Always show the result of every unit test when running via command line, even if it passes.
    testOptions.unitTests {
        includeAndroidResources = true

        // ... 
    }

O alerta "WARN: Android SDK 29 requires Java 9..." é mais complicado. A execução de testes no Android Q requer o Java 9. Em vez de tentar configurar o Android Studio para usar o Java 9, para este codelab, mantenha o SDK de destino e o build em 28.

Resumindo:

  • Geralmente, os testes de modelo de visualização puro podem ser inseridos no conjunto de origem test porque o código geralmente não exige o Android.
  • Você pode usar a biblioteca AndroidX test para ver versões de teste de componentes, como apps e atividades.
  • Se você precisar executar um código Android simulado no seu conjunto de origem test, poderá adicionar a dependência do Robolectric e a anotação @RunWith(AndroidJUnit4::class).

Parabéns! Você está usando a biblioteca de testes do AndroidX e o Robolectric para executar um teste. Seu teste ainda não foi concluído. Você ainda não escreveu uma declaração de declaração. Ela diz apenas // TODO test LiveData. Você aprenderá a escrever instruções de declaração com LiveData em seguida.

Nesta tarefa, você aprenderá a declarar corretamente o valor do LiveData.

Aqui você parou sem addNewTask_setsNewTaskEvent testar o modelo de visualização.

TasksViewModelTest.kt (link em inglês)

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
    

Para testar o LiveData, recomendamos que você faça duas coisas:

  1. Use InstantTaskExecutorRule
  2. Garanta a observação em LiveData

Etapa 1. Usar InstantTaskExecutorRule

InstantTaskExecutorRule é uma regra JUnit. Quando você o usa com a anotação @get:Rule, ele faz com que alguns códigos da classe InstantTaskExecutorRule sejam executados antes e depois dos testes. Para ver o código exato, use o atalho de teclado Command+B para ver o arquivo.

Essa regra executa todos os jobs em segundo plano relacionados aos Componentes de arquitetura na mesma linha de execução para que os resultados do teste aconteçam de maneira síncrona e em uma ordem reproduzível. Ao criar testes que incluam testes de LiveData, use esta regra

  1. Adicione a dependência do Gradle à biblioteca de testes principais de Componentes da arquitetura, que contém essa regra.

app/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. Abrir TasksViewModelTest.kt
  2. Adicione o InstantTaskExecutorRule à classe TasksViewModelTest.

TasksViewModelTest.kt (link em inglês)

class TasksViewModelTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    // Other code...
}

Etapa 2: Adicionar a classe LiveDataTestUtil.kt

A próxima etapa é verificar se o LiveData que você está testando é observado.

Ao usar o LiveData, você normalmente tem uma atividade ou um fragmento (LifecycleOwner) que observa o LiveData.

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
})

Essa observação é importante. Você precisa de observadores ativos em LiveData para

Para ter o comportamento LiveData esperado do LiveData do modelo de visualização, é necessário observar o LiveData com uma LifecycleOwner.

Isso apresenta um problema: no teste de TasksViewModel, não há uma atividade ou um fragmento para observar o LiveData. Para contornar isso, você pode usar o método observeForever, que garante que o LiveData seja observado constantemente, sem precisar de um LifecycleOwner. Ao observeForever, você precisa remover seu observador ou correr o risco de vazamento.

O código é semelhante ao código abaixo. Examine-o:

@Test
fun addNewTask_setsNewTaskEvent() {

    // Given a fresh ViewModel
    val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())


    // Create observer - no need for it to do anything!
    val observer = Observer<Event<Unit>> {}
    try {

        // Observe the LiveData forever
        tasksViewModel.newTaskEvent.observeForever(observer)

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        // Whatever happens, don't forget to remove the observer!
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}

Ele é um código boilerplate para observar um único LiveData em um teste. Há algumas maneiras de se livrar desse código clichê. Você criará uma função de extensão chamada LiveDataTestUtil para facilitar a adição de observadores.

  1. Crie um novo arquivo Kotlin com o nome LiveDataTestUtil.kt no conjunto de origem test.


  1. Copie e cole o código abaixo.

LiveDataTestUtil.kt.

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

Esse é um método bastante complicado. Ela cria uma função de extensão Kotlin chamada getOrAwaitValue, que adiciona um observador, recebe o valor LiveData e, em seguida, limpa o observador. Basicamente, é uma versão curta e reutilizável do código observeForever mostrado acima. Para ver uma explicação completa sobre ela, confira esta postagem do blog.

Etapa 3. Usar getOrAwaitValue para escrever a declaração

Nesta etapa, você usa o método getOrAwaitValue e escreve uma instrução de declaração que verifica se o newTaskEvent foi acionado.

  1. Consiga o valor LiveData para newTaskEvent usando getOrAwaitValue.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. Afirmar que o valor não é nulo.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

O teste completo ficará como o exemplo abaixo.

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()


    @Test
    fun addNewTask_setsNewTaskEvent() {
        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

        assertThat(value.getContentIfNotHandled(), not(nullValue()))


    }

}
  1. Execute o código e veja o passe de teste.

Agora que você já viu como criar um teste, crie um por conta própria. Nesta etapa, usando as habilidades que você aprendeu, escreva outro teste no TasksViewModel.

Etapa 1. Criar seu próprio teste de ViewModel

Você escreverá setFilterAllTasks_tasksAddViewVisible(). Esse teste verifica se o botão Adicionar tarefa está visível se você definiu o tipo de filtro para mostrar todas as tarefas.

  1. Usando addNewTask_setsNewTaskEvent() para referência, escreva um teste em TasksViewModelTest com o nome setFilterAllTasks_tasksAddViewVisible() que define o modo de filtragem como ALL_TASKS e declara que o LiveData do tasksAddViewVisible é true.


Use o código abaixo para começar.

TasksViewModelTest (link em inglês)

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

        // Then the "Add task" action is visible
        
    }

Observação:

  • A enumeração TasksFilterType para todas as tarefas é ALL_TASKS.
  • A visibilidade do botão para adicionar uma tarefa é controlada pela LiveData tasksAddViewVisible..
  1. Execute o teste.

Etapa 2: Comparar o teste com a solução

Compare sua solução com a solução abaixo.

TasksViewModelTest (link em inglês)

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }

Faça o seguinte:

  • Crie tasksViewModel usando a mesma instrução ApplicationProvider.getApplicationContext() do AndroidX.
  • Você chama o método setFiltering, transmitindo a enumeração do tipo de filtro ALL_TASKS.
  • Verifique se tasksAddViewVisible é verdadeiro usando o método getOrAwaitNextValue.

Etapa 3. Adicionar uma regra @Before

Observe que, no início dos dois testes, você define um TasksViewModel.

TasksViewModelTest (link em inglês)

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Quando tiver o código de configuração repetido para vários testes, você poderá usar a anotação @Before para criar um método de configuração e remover o código repetido. Como todos esses testes vão testar o TasksViewModel e precisam de um modelo de visualização, mova esse código para um bloco @Before.

  1. Crie uma variável de instância lateinit com o nome tasksViewModel|.
  2. Crie um método com o nome setupViewModel.
  3. Adicione a anotação @Before.
  4. Mova o código de instanciação do modelo de visualização para setupViewModel.

TasksViewModelTest (link em inglês)

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. Execute o código.

Aviso

Não faça o seguinte, não inicialize o

tasksViewModel

com a respectiva definição:

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Isso fará com que a mesma instância seja usada para todos os testes. Isso é algo que você deve evitar, porque cada teste deve ter uma nova instância do objeto sendo testado (o ViewModel, nesse caso).

O código final da TasksViewModelTest ficará assim:

TasksViewModelTest

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }


    @Test
    fun addNewTask_setsNewTaskEvent() {

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.awaitNextValue()
        assertThat(
            value?.getContentIfNotHandled(), (not(nullValue()))
        )
    }

    @Test
    fun getTasksAddViewVisible() {

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
    }
    
}

Clique aqui para ver uma diferença entre o código iniciado e o código final.

Para fazer o download do código do codelab concluído, use o comando git abaixo:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1


Como alternativa, é possível fazer o download do repositório como um arquivo ZIP, descompactá-lo e abri-lo no Android Studio.

Fazer o download do ZIP

Este codelab falou sobre:

  • Saber executar testes do Android Studio.
  • A diferença entre testes locais (test) e de instrumentação (androidTest).
  • Como criar testes de unidade locais usando o JUnit e o Hamcrest.
  • Configurar testes do ViewModel com a Biblioteca AndroidX Test.

Curso da Udacity:

Documentação do desenvolvedor Android:

Vídeos:

Outro:

Para ver links de outros codelabs neste curso, consulte a página de destino dos codelabs avançados no Android.