Princípios básicos dos testes

Este codelab faz parte do curso Android avançado no Kotlin. Você vai aproveitar mais este 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 do Android avançado em Kotlin.

Introdução

Ao implementar o primeiro recurso do seu primeiro app, você provavelmente executou o código para verificar se ele funcionava como esperado. Você realizou um teste, embora um teste manual. À medida que você adicionava e atualizava recursos, provavelmente também continuava executando e verificando se o código funcionava. Mas fazer isso manualmente toda vez é cansativo, propenso a erros e não é escalonável.

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

Nesta série de codelabs, você vai aprender a criar uma coleção de testes (conhecida como conjunto de testes) para um app do mundo real.

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

O que você já precisa saber

Você precisa:

O que você vai aprender

Você vai 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ê vai aprender sobre as seguintes bibliotecas e conceitos de programação:

Atividades deste laboratório

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

Nesta série de codelabs, você vai trabalhar com o app TO-DO Notes. Com ele, é possível anotar tarefas a serem concluídas e mostrá-las em uma lista. Depois, você pode marcar como concluídas ou não, filtrar ou excluir.

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

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:

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

Nesta tarefa, você vai executar o app e analisar a base de código.

Etapa 1: executar o app de exemplo

Depois de baixar o app de tarefas, abra e execute no Android Studio. Ele será compilado. Para explorar o app, faça o seguinte:

  • Crie uma tarefa com o botão de ação flutuante de adição. Primeiro, digite um título e depois adicione mais informações sobre a tarefa. Salve com o FAB de marca de seleção verde.
  • Na lista de tarefas, clique no título da tarefa que você acabou de concluir e confira a tela de detalhes 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 de tarefas, abra o menu de filtro e filtre as tarefas por status Ativa e Concluída.
  • Abra a gaveta de navegação e clique em Estatísticas.
  • Volte para a tela de visão geral e, no menu gaveta de navegação, selecione Limpar concluídas para excluir todas as tarefas com o status Concluída.

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

O app de tarefas pendentes é baseado no popular exemplo de teste e arquitetura Architecture Blueprints (usando 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 o Room. Se você conhece algum dos exemplos abaixo, saiba que este app tem uma arquitetura semelhante:

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

Confira o resumo dos pacotes disponíveis:

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

.addedittask

Tela de adicionar ou editar uma tarefa:código da camada de UI para adicionar ou editar uma tarefa.

.data

A 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 UI para a tela de estatísticas.

.taskdetail

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

.tasks

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

.util

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

Camada de dados (.data)

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

O DefaultTasksRepository coordena ou faz a mediação entre a camada de rede e a camada de banco de dados, sendo o que retorna dados para a camada de UI.

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

Cada um dos pacotes da camada de UI contém um fragmento e um modelo de visualização, além de outras classes necessárias para a UI, 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 Navigation. Ele é definido 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 os 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. Os conjuntos de origem são pastas que contêm o código-fonte do app. Os conjuntos de origem, que são coloridos em verde (androidTest e test), contêm seus testes. Ao criar um 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 versões diferentes do app que você pode criar (conhecidas como variantes de build).
  • androidTest: contém testes conhecidos como testes instrumentados.
  • test: contém testes conhecidos como testes locais.

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

Testes locais (conjunto de origem test)

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, eles são executados rapidamente, mas a fidelidade é menor, ou seja, eles agem menos como fariam 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 (conjunto de origem androidTest)

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

No Android Studio, os testes instrumentados 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 do mouse e selecione Run ExampleUnitTest.

Você vai ver a seguinte saída na janela Executar na parte de baixo 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. Que bom saber que a adição funciona conforme o esperado!

Etapa 2: fazer o teste falhar

Confira abaixo o teste que você acabou de executar.

ExampleUnitTest.kt

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

Observe que os testes

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

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

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

Para ver como é um teste com falha, adicione uma declaração que você pode ver facilmente que vai falhar. Ele vai verificar se 3 é igual a 1+1.

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

ExampleUnitTest.kt

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 do teste, observe um X ao lado dele.

  1. Observe também:
  • Uma única declaração com falha faz com que todo o teste falhe.
  • Você recebe o valor esperado (3) e o valor calculado (2).
  • Você será direcionado à linha da asserção com falha (ExampleUnitTest.kt:16).

Etapa 3: executar um teste instrumentado

Os testes instrumentados estão no conjunto de origem androidTest.

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

ExampleInstrumentedTest

@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, um smartphone Pixel 2 emulado):

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

Nesta tarefa, você vai escrever testes para getActiveAndCompleteStats, que calcula a porcentagem de estatísticas de tarefas ativas e concluídas do seu app. Esses números podem ser vistos na tela de estatísticas do app.

Etapa 1: criar uma classe de teste

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

StatisticsUtils.kt

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 de tarefas ativas.

O Android Studio oferece ferramentas para gerar stubs de teste que ajudam a implementar os testes dessa função.

  1. Clique com o botão direito do mouse em getActiveAndCompletedStats e selecione Generate > Test.

A caixa de diálogo Criar teste é aberta:

  1. Mude o Nome da classe para StatisticsUtilsTest (em vez de StatisticsUtilsKtTest. É um pouco melhor não ter KT no nome da classe de teste).
  2. Mantenha os demais padrões. O JUnit 4 é a biblioteca de testes adequada. O pacote de destino está correto (ele reflete o local da classe StatisticsUtils) e não é necessário marcar nenhuma das caixas de seleção (isso apenas gera código extra, mas você vai escrever seu teste do zero).
  3. Pressione OK.

A caixa de diálogo Escolher diretório de destino é aberta:

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

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

Etapa 2: escrever sua primeira função de teste

Você vai escrever um teste que verifica:

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

StatisticsUtilsTest.kt

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 é 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 que você esperava usando asserções.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

Veja o código completo.

StatisticsUtilsTest.kt

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 Executar).

Ele precisa passar:

Etapa 3: adicionar a dependência do Hamcrest

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

assertEquals(result.completedTasksPercent, 0f)

// versus

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

A segunda afirmação parece muito mais uma frase humana. Ele é escrito usando uma estrutura de asserção chamada Hamcrest. Outra ferramenta útil para escrever declarações legíveis é a biblioteca Truth. Neste codelab, você vai usar o Hamcrest para escrever asserçõ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 você está usando testImplementation. Quando você estiver pronto para compartilhar seu app com o mundo, é melhor não aumentar o tamanho do APK com nenhum código de teste ou dependência no app. Você pode designar se uma biblioteca deve ser incluída no código principal ou de teste usando configurações do Gradle. As configurações mais comuns são:

  • implementation: a dependência está disponível em todos os conjuntos de origem, incluindo os 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.

A configuração usada 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. Isso também garante que o Hamcrest não será incluído no app final.

Etapa 4: usar o Hamcrest para escrever asserçõ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))

Observação: use a importação import org.hamcrest.Matchers.`is` se solicitado.

O teste final vai ficar assim:

StatisticsUtilsTest.kt

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 vai ensinar todos os detalhes do Hamcrest. Se quiser saber mais, confira o tutorial oficial.

Esta é uma tarefa opcional para praticar.

Nela, você vai escrever mais testes usando JUnit e Hamcrest. Você também vai escrever testes usando uma estratégia derivada da prática de programação de desenvolvimento orientado a testes. O desenvolvimento orientado a testes (TDD) é uma escola de pensamento de programação que diz que, em vez de escrever o código do recurso primeiro, você escreve os testes primeiro. Em seguida, você escreve o código do 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 ativa, a porcentagem de activeTasks será 0f, e a porcentagem de tarefas concluídas será 100f .
  2. Se houver duas tarefas concluídas e três ativas, a porcentagem concluída será 40f e a porcentagem ativa será 60f.

Etapa 2. Criar um teste para um bug

O código do getActiveAndCompletedStats escrito tem um bug. Observe como ele não processa corretamente o que acontece se a lista estiver vazia ou nula. Em ambos os 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 escrever testes, você vai usar o desenvolvimento orientado a testes. O desenvolvimento orientado a testes segue estas etapas.

  1. Escreva o teste usando a estrutura "Dado, Quando, Então" e com um nome que siga a convenção.
  2. Confirme se o teste falhou.
  3. Escreva o mínimo de código para que o teste seja aprovado.
  4. Repita para todos os testes.

Em vez de começar corrigindo o bug, você vai começar escrevendo os testes primeiro. Assim, você pode confirmar que tem testes que protegem contra a reintrodução acidental desses bugs no futuro.

  1. Se houver uma lista vazia (emptyList()), as duas porcentagens serão 0f.
  2. Se houver 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ê tem os testes, corrija o bug.

  1. Corrija o bug em getActiveAndCompletedStats retornando 0f se tasks for null ou 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 os testes novamente e confirme se todos foram aprovados.

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

  • Novas funcionalidades sempre têm testes associados. Assim, os testes funcionam como documentação do que o código faz.
  • Seus testes verificam os resultados corretos e protegem contra bugs que você já viu.

Solução: escrever mais testes

Confira todos os testes e o código do recurso correspondente.

StatisticsUtilsTest.kt

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

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
        )
    }
}

Você fez um ótimo trabalho com os princípios básicos de criação e execução de testes. Em seguida, você vai aprender a escrever testes básicos de ViewModel e LiveData.

No restante do codelab, você vai aprender a escrever testes para duas classes do Android que são comuns na maioria dos apps: ViewModel e LiveData.

Comece escrevendo testes para o TasksViewModel.


Você vai se concentrar em testes que têm toda a lógica no 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, o que aumenta a complexidade dos testes. Por enquanto, vamos evitar isso e focar na criação de testes para a funcionalidade do ViewModel que não testa diretamente nada no repositório.



O teste que você vai escrever verifica se, ao chamar o método addNewTask, o Event para abrir a nova janela de tarefa é acionado. Este é 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 que você fez para StatisticsUtilTest, nesta etapa, crie 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 -> Gerar -> Teste.

  1. Na tela Criar teste, clique em OK para aceitar (não é necessário mudar nenhuma das configurações padrão).
  2. Na caixa de diálogo Choose Destination Directory, escolha o diretório test.

Etapa 2. Começar a escrever o teste do ViewModel

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

  1. Crie um teste chamado addNewTask_setsNewTaskEvent.

TasksViewModelTest.kt

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?

Ao criar uma instância de TasksViewModel para teste, o construtor dela exige um contexto do aplicativo. Mas, neste teste, você não está criando um aplicativo completo com atividades, interface e fragmentos. Então, como você consegue um contexto de aplicativo?

TasksViewModelTest.kt

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

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

  1. Adicionar as dependências principais e ext do AndroidX Test
  2. Adicione a dependência da biblioteca de testes Robolectric.
  3. Adicione a anotação do executor de testes AndroidJunit4 à classe
  4. Escrever código do AndroidX Test

Você vai concluir essas etapas e depois entender o que elas fazem juntas.

Etapa 3. Adicionar as dependências do Gradle

  1. Copie essas dependências para o arquivo build.gradle do módulo do app para adicionar as dependências principais do AndroidX Test Core e ext, além da dependência de teste do 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 o executor de testes do JUnit

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

TasksViewModelTest.kt

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

Etapa 5. Usar o AndroidX Test

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

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

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. Chame addNewTask no número tasksViewModel.

TasksViewModelTest.kt

tasksViewModel.addNewTask()

Neste ponto, seu teste vai ficar parecido com o código abaixo.

TasksViewModelTest.kt

    @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 o teste para confirmar se ele funciona.

Conceito: como o AndroidX Test funciona?

O que é o AndroidX Test?

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

ApplicationProvider.getApplicationContext()

Um dos benefícios das APIs AndroidX Test é que elas foram criadas para funcionar tanto em testes locais quanto em testes instrumentados. Isso é bom porque:

  • É possível executar o mesmo teste como um teste local ou instrumentado.
  • Não é necessário aprender diferentes APIs de teste para testes locais e instrumentados.

Por exemplo, como você escreveu o código usando as bibliotecas do AndroidX Test, é possível mover a classe TasksViewModelTest da pasta test para a pasta androidTest, e os testes ainda serão executados. O getApplicationContext() funciona de maneira um pouco diferente dependendo se está sendo executado como um teste local ou instrumentado:

  • Se for um teste instrumentado, ele vai receber o contexto real do aplicativo fornecido quando um emulador é inicializado ou um dispositivo real é conectado.
  • Se for um teste local, ele usará um ambiente Android simulado.

O que é o Robolectric?

O ambiente simulado do Android usado pelo AndroidX Test para testes locais é fornecido pelo Robolectric. O Robolectric é uma biblioteca que cria um ambiente Android simulado para testes e é executada mais rápido do que iniciar um emulador ou executar em um dispositivo. Sem a dependência do Robolectric, você vai receber este erro:

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

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

O executor de testes AndroidJUnit4 permite que o AndroidX Test execute seus testes de maneira diferente, dependendo se eles são instrumentados ou locais.

Etapa 6: Corrigir avisos do Robolectric

Ao executar o código, observe que o Robolectric é usado.

Devido ao AndroidX Test e ao executor de testes AndroidJunit4, isso é feito sem que você escreva diretamente uma única linha de código do Robolectric.

Talvez você veja 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 arquivo 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 aviso "WARN: Android SDK 29 requires Java 9..." é mais complicado. Para executar testes no Android Q, é necessário o Java 9. Em vez de tentar configurar o Android Studio para usar o Java 9, mantenha o SDK de destino e de compilação em 28 neste codelab.

Em resumo:

  • Os testes de modelo de visualização pura geralmente podem ser incluídos no conjunto de origem test porque o código deles não costuma exigir o Android.
  • Você pode usar a bibliotecaAndroidX test para receber versões de teste de componentes como aplicativos e atividades.
  • Se você precisar executar código Android simulado no conjunto de origem test, adicione 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 não está concluído (você ainda não escreveu uma instrução assert, apenas // TODO test LiveData). Em seguida, você vai aprender a escrever instruções assert com LiveData.

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

Foi aqui que você parou sem o teste do modelo de visualização addNewTask_setsNewTaskEvent.

TasksViewModelTest.kt

    @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 LiveData, recomendamos que você faça duas coisas:

  1. Usar InstantTaskExecutorRule
  2. Garantir a observação de LiveData

Etapa 1. Usar InstantTaskExecutorRule

InstantTaskExecutorRule é uma regra do JUnit. Quando usado com a anotação @get:Rule, ele faz com que parte do código na classe InstantTaskExecutorRule seja executada antes e depois dos testes. Para ver o código exato, use o atalho de teclado Command+B.

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 ocorram de forma síncrona e em uma ordem repetível. Use esta regra ao escrever testes que incluem o LiveData.

  1. Adicione a dependência do Gradle para a biblioteca de testes principais dos componentes de 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

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

Etapa 2. Adicionar a classe LiveDataTestUtil.kt

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

Ao usar LiveData, é comum ter uma atividade ou um fragmento (LifecycleOwner) observando o LiveData.

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

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

Para ter o comportamento LiveData esperado para o LiveData do modelo de visualização, observe o LiveData com um LifecycleOwner.

Isso causa um problema: no teste de TasksViewModel, você não tem uma atividade ou um fragmento para observar o LiveData. Para evitar isso, use o método observeForever, que garante que o LiveData seja constantemente observado, sem precisar de um LifecycleOwner. Ao observeForever, não se esqueça de remover o observador para evitar um vazamento.

Isso fica parecido com o código abaixo. Analise:

@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)
    }
}

É muito código boilerplate para observar um único LiveData em um teste. Há algumas maneiras de se livrar desse texto padrão. Você vai criar uma função de extensão chamada LiveDataTestUtil para simplificar a adição de observadores.

  1. Crie um arquivo Kotlin chamado 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. Ele cria uma função de extensão do Kotlin chamada getOrAwaitValue, que adiciona um observador, recebe o valor LiveData e limpa o observador. Basicamente, é uma versão curta e reutilizável do código observeForever mostrado acima. Para uma explicação completa dessa classe, confira esta postagem do blog (em inglês).

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

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

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

O teste completo vai ficar parecido com o código 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 teste ser aprovado.

Agora que você já sabe como escrever um teste, crie um por conta própria. Nesta etapa, usando as habilidades que você aprendeu, pratique escrever outro teste TasksViewModel.

Etapa 1. Criar seu próprio teste do ViewModel

Você vai escrever setFilterAllTasks_tasksAddViewVisible(). Esse teste precisa verificar se o botão Adicionar tarefa está visível quando o tipo de filtro está definido para mostrar todas as tarefas.

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


Use o código abaixo para começar.

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

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

Observação:

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

Etapa 2. Compare seu teste com a solução

Compare sua solução com a abaixo.

TasksViewModelTest

    @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))
    }

Verifique se você faz o seguinte:

  • Crie seu 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.
  • Você verifica se o tasksAddViewVisible é verdadeiro usando o método getOrAwaitNextValue.

Etapa 3. Adicionar uma regra @Before

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

TasksViewModelTest

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

Quando você tem um código de configuração repetido para vários testes, pode 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 chamada tasksViewModel|.
  2. Crie um método com o nome setupViewModel.
  3. Anote-o com @Before.
  4. Mova o código de instanciação do modelo de visualização para setupViewModel.

TasksViewModelTest

    // 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 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 precisa ter uma nova instância do sujeito em teste (o ViewModel, neste caso).

O código final de TasksViewModelTest vai 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 inicial e o final.

Para baixar o 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


Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.

Fazer o download do ZIP

Este codelab abordou:

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

Curso da Udacity:

Documentação do desenvolvedor Android:

Vídeos:

Outro:

Para acessar links de outros codelabs deste curso, consulte a página inicial dos codelabs do curso Android avançado no Kotlin.