Introdução aos testes duplos e injeção de dependência

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

Este segundo codelab de teste trata sobre testes duplos: quando usá-los no Android e como implementá-los usando a injeção de dependência, o padrão Localizador de serviços e bibliotecas. Ao fazer isso, você aprenderá a escrever:

  • Testes de unidade de repositório
  • Fragmentos e testes de integração de viewmodel
  • Testes de navegação de fragmentos

O que você já precisa saber

Você precisa:

O que você vai aprender

  • Como planejar uma estratégia de testes
  • Como criar e usar testes duplos, ou seja, simulações e simulações
  • Como usar a injeção de dependência manual no Android para testes de unidade e integração.
  • Como aplicar o padrão do localizador de serviços
  • Como testar repositórios, fragmentos, modelos de visualização e o componente de navegação.

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

Atividades do laboratório

  • gravar testes de unidade para um repositório usando um teste duplo e injeção de dependência;
  • Criar testes de unidade para um modelo de visualização usando um teste duplo e injeção de dependência.
  • Programar testes de integração para fragmentos e modelos de visualização usando o framework de testes de IU do Espresso.
  • Crie testes de navegação usando o Mockito e o Espresso.

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 foi escrito em Kotlin, tem algumas 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.

Fazer o download do código

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 end_codelab_1

Reserve um momento para se familiarizar com o código, seguindo as instruções abaixo.

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.

Neste codelab, você aprenderá a testar repositórios, visualizar modelos e fragmentos usando testes duplos e injeção de dependência. Antes de nos aprofundarmos neles, é importante entender o que motivará você a programar os testes.

Esta seção aborda algumas práticas recomendadas dos testes em geral, já que elas se aplicam ao Android.

Pirâmide de teste

Ao pensar em uma estratégia de testes, há três aspectos relacionados:

  • Escopo: quanto do código o teste toca? Os testes podem ser executados com um único método, em todo o aplicativo ou em algum lugar entre eles.
  • Velocidade: qual é a velocidade do teste? As velocidades de teste podem variar de milissegundos a vários minutos.
  • Fidelidade: até que ponto é o teste real? Por exemplo, se parte do código que você está testando precisar fazer uma solicitação de rede, o código do teste faz essa solicitação de rede ou é um resultado falso? Se o teste realmente se comunicar com a rede, significa que ele tem maior fidelidade. A desvantagem é que o teste pode levar mais tempo para ser executado, pode resultar em erros caso a rede esteja inativa ou possa ser caro.

Há vantagens e desvantagens inerentes entre esses aspectos. Por exemplo, velocidade e fidelidade são compensadas. Quanto mais rápido o teste, geralmente, menos fidelidade e vice-versa. Uma maneira comum de dividir testes automatizados é nestas três categorias:

  • Testes de unidade: são testes altamente focados em uma única classe, geralmente um único método nessa classe. Se um teste de unidade falhar, você saberá exatamente onde o problema está no código. A fidelidade deles é baixa porque, no mundo real, seu app envolve muito mais do que a execução de um método ou uma classe. Eles são rápidos o suficiente para serem executados sempre que você altera o código. Na maioria das vezes, eles serão executados localmente no conjunto de origem test. Exemplo: como testar métodos únicos em modelos de visualização e repositórios.
  • Testes de integração: testam a interação de várias classes para garantir que elas se comportem conforme esperado quando usadas juntas. Uma maneira de estruturar testes de integração é fazer com que eles testem um único recurso, como a capacidade de salvar uma tarefa. Eles testam um escopo de código maior que os testes de unidade, mas ainda são otimizados para funcionar rapidamente em comparação com a fidelidade total. Eles podem ser executados localmente ou como testes de instrumentação, dependendo da situação. Exemplo: como testar toda a funcionalidade de um único fragmento e visualizar o par de modelos.
  • Testes completos (E2e): teste uma combinação de recursos funcionando em conjunto. Eles testam grandes partes do app, simulam o uso real de perto e, portanto, geralmente são lentos. Eles têm a maior fidelidade e informam que seu aplicativo realmente funciona como um todo. De modo geral, esses testes serão instrumentados (no conjunto de origem de androidTest).
    Exemplo: inicializar todo o app e testar alguns recursos juntos.

A proporção sugerida desses testes geralmente é representada por uma pirâmide, com a grande maioria dos testes sendo testes de unidade.

Arquitetura e teste

Sua capacidade de testar seu aplicativo em todos os diferentes níveis da pirâmide de teste está inerentemente ligada à arquitetura do seu aplicativo. Por exemplo, um aplicativo extremamente com arquitetura incorreta pode colocar toda a lógica dentro de um método. Você pode programar um teste completo para isso, já que eles geralmente testam grande parte do app, mas e quanto a testes de unidade ou de integração? Com todo o código em um só lugar, é difícil testar apenas o código relacionado a uma única unidade ou recurso.

Uma abordagem melhor seria dividir a lógica do aplicativo em vários métodos e classes, permitindo que cada parte seja testada isoladamente. A arquitetura é uma maneira de dividir e organizar o código, o que facilita o teste de unidade e integração. O app de tarefas que você testará segue uma arquitetura específica:



Nesta lição, você verá como testar partes da arquitetura acima de forma adequada:

  1. Primeiro você fará o teste da unidade do repositório.
  2. Em seguida, você usará um teste duplo no modelo de visualização, que é necessário para o teste de unidade e o teste de integração do modelo de visualização.
  3. Em seguida, você aprenderá a criar testes de integração para fragmentos e modelos de visualização.
  4. Por fim, você vai aprender a criar testes de integração que incluem o componente de navegação.

O teste completo será abordado na próxima lição.

Quando você escreve um teste de unidade para uma parte de uma classe (um método ou um pequeno conjunto de métodos), a meta é testar somente o código dessa classe.

Testar apenas o código em classes específicas pode ser complicado. Veja um exemplo. Abra a classe data.source.DefaultTaskRepository no conjunto de origem main. Esse é o repositório do app. Essa é a classe em que você criará testes de unidade para o próximo caso.

Seu objetivo é testar somente o código dessa classe. Ainda assim, DefaultTaskRepository depende de outras classes, como LocalTaskDataSource e RemoteTaskDataSource, para funcionar. Em outras palavras, LocalTaskDataSource e RemoteTaskDataSource são dependências de DefaultTaskRepository.

Portanto, todos os métodos em DefaultTaskRepository chamam métodos em classes de fonte de dados, que, por sua vez, chamam métodos em outras classes para salvar informações em um banco de dados ou se comunicar com a rede.



Por exemplo, veja este método em DefaultTasksRepo.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks é uma das chamadas mais "quot;basic" que você pode fazer para seu repositório. Esse método inclui ler um banco de dados SQLite e fazer chamadas de rede (a chamada para updateTasksFromRemoteDataSource). Isso envolve muito mais código do que apenas o código do repositório.

Veja alguns motivos mais específicos para dificultar o teste do repositório:

  • Você precisa pensar em como criar e gerenciar um banco de dados para realizar até os testes mais simples. Isso traz perguntas como "isso deve ser um teste local ou instrumentado?" e se você deveria usar o AndroidX Test para ter um ambiente Android simulado.
  • Algumas partes do código, como o código de rede, podem demorar muito para serem executadas ou até mesmo apresentarem falhas ao criar testes lentos e de longa duração.
  • Os testes podem perder a capacidade de diagnosticar qual código é responsável por uma falha. Os testes podem começar a testar códigos que não são de repositório, por isso, por exemplo, os supostos testes de unidade podem ocorrer devido a um problema em alguns dos códigos dependentes, como o código de banco de dados.

Duplas de teste

A solução para isso é que, ao testar o repositório, não use o código real do banco de dados ou da rede, mas, em vez disso, use um teste duplo. Um teste duplo é uma versão de uma classe criada especificamente para testes. Ele substitui a versão real de uma classe em testes. É semelhante à forma como um dublê é um ator especializado em acrobacias e substitui o ator real por ações perigosas.

Veja alguns tipos de dupla de testes:

Falso

Um double de teste que tenha uma implementação "funcionando” da classe, mas que seja implementado de forma que seja boa para testes, mas não adequado para produção.

Simulação

Um teste duplo que rastreia quais métodos foram chamados. Em seguida, ele é aprovado ou reprovado em um teste, dependendo de se os métodos foram chamados corretamente.

Stub (em inglês)

Um teste duplo que não inclui lógica e retorna apenas o que você programou para retornar. Um StubTaskRepository pode ser programado para retornar determinadas combinações de tarefas de getTasks, por exemplo.

Dummy

Um teste duplo que é transmitido, mas não usado, como se você só precisa fornecê-lo como um parâmetro. Se você tivesse um NoOpTaskRepository, ele simplesmente implementaria o TaskRepository sem códigos em nenhum dos métodos.

Espionagem

Um teste duplo que também monitora algumas informações adicionais. Por exemplo, se você criou uma SpyTaskRepository, ele pode rastrear o número de vezes que o método addTask foi chamado.

Para saber mais sobre duplas de teste, confira Testes no vaso sanitário: conheça suas duplas de teste.

As duplicações mais comuns usadas no Android são Fakes e Mocks.

Nesta tarefa, você criará um teste FakeDataSource duplo para o teste de unidade DefaultTasksRepository separado das fontes de dados reais.

Etapa 1: criar a classe FakeDataSource

Nesta etapa, você criará uma classe com o nome FakeDataSouce, que será um teste duplo de LocalDataSource e RemoteDataSource.

  1. No conjunto de origem test, clique com o botão direito do mouse e selecione New -> Package.

  1. Crie um pacote data com um pacote source.
  2. Crie uma nova classe chamada FakeDataSource no pacote data/source.

Etapa 2: implementar a interface TasksDataSource

Para poder usar a nova classe FakeDataSource como teste duplo, ela precisa substituir as outras fontes de dados. Essas fontes são TasksLocalDataSource e TasksRemoteDataSource.

  1. Observe como esses dois implementam a interface TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Faça com que FakeDataSource implemente TasksDataSource:
class FakeDataSource : TasksDataSource {

}

O Android Studio reclamará que você não implementou os métodos necessários para TasksDataSource.

  1. Use o menu de correção rápida e selecione Implementar membros.


  1. Selecione todos os métodos e pressione OK.

Etapa 3: implementar o método getTasks no FakeDataSource

FakeDataSource é um tipo específico de teste duplo chamado de falso. Falso é um conjunto duplo de testes que tem uma implementação "funcionando” da classe, mas é implementado de uma forma que o torna adequado para testes, mas inadequado para produção. "Trabalho" significa que a classe produzirá saídas realistas de acordo com as entradas.

Por exemplo, a fonte de dados falsa não se conectará à rede nem salvará nada em um banco de dados. Em vez disso, ela usará apenas uma lista na memória. Isso funcionará como esperado nos métodos para receber ou salvar tarefas. No entanto, essa implementação nunca será usada, porque ela não é salva no servidor ou em um banco de dados.

Um FakeDataSource

  • permite que você teste o código no DefaultTasksRepository sem precisar depender de um banco de dados ou de uma rede real.
  • oferece uma implementação "quão real" para os testes.
  1. Mude o construtor FakeDataSource para criar um var chamado tasks, que é um MutableList<Task>? com um valor padrão de uma lista mutável vazia.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Esta é a lista de tarefas que "são" respostas a um banco de dados ou a um servidor. Por enquanto, a meta é testar o método getTasks do repositório. Isso chama os métodos fonte de dados getTasks, deleteAllTasks e saveTask.

Grave uma versão falsa destes métodos:

  1. Escreva getTasks: se tasks não for null, retorne um resultado Success. Se tasks for null, retorna um resultado Error.
  2. Gravar deleteAllTasks: limpe a lista de tarefas mutáveis.
  3. Escrever saveTask: adicione a tarefa à lista.

Esses métodos, implementados para FakeDataSource, são semelhantes ao código abaixo.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

Veja as instruções de importação, se necessário:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

Isso é parecido com o funcionamento das fontes de dados locais e remotas.

Nesta etapa, você usará uma técnica chamada "injeção manual de dependência" para usar o dobro do teste falso que acabou de criar.

O principal problema é que você tem um FakeDataSource, mas não está claro como ele é usado nos testes. Ele precisa substituir a TasksRemoteDataSource e o TasksLocalDataSource, mas apenas nos testes. Tanto TasksRemoteDataSource quanto TasksLocalDataSource são dependências de DefaultTasksRepository, o que significa que DefaultTasksRepositories requer ou "dependente" dessas classes para ser executado.

No momento, as dependências são criadas dentro do método init do DefaultTasksRepository.

DefaultTasksRepository.kt (link em inglês)

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

Como você está criando e atribuindo taskLocalDataSource e tasksRemoteDataSource dentro de DefaultTasksRepository, eles são essencialmente codificados. Não é possível trocar o teste duplo.

O que você quer fazer é fornecer essas origens de dados à classe em vez de codificá-las. O fornecimento de dependências é conhecido como injeção de dependência. Há maneiras diferentes de fornecer dependências e, portanto, diferentes tipos de injeção de dependência.

A Builder Dependency Injection permite alternar no teste duplo transmitindo-a para o construtor.

Sem injeção

Injeção

Etapa 1: usar a injeção de dependência do construtor em DefaultTasksRepository

  1. Mude o construtor do DefaultTaskRepository para incluir uma Application nas duas origens de dados e no agente de corrotina. Você também precisará fazer isso para seus testes. Isso é descrito em mais detalhes na terceira seção da aula sobre corrotinas.

DefaultTasksRepository.kt (link em inglês)

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Como você transmitiu as dependências, remova o método init. Não é mais necessário criar as dependências.
  2. Exclua também as antigas variáveis de instância. Você os define no construtor:

DefaultTasksRepository.kt (link em inglês)

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Por fim, atualize o método getRepository para usar o novo construtor:

DefaultTasksRepository.kt (link em inglês)

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

Agora você está usando a injeção de dependência do construtor.

Etapa 2: usar o FakeDataSource nos testes

Agora que o código está usando a injeção de dependência do construtor, você pode usar uma origem de dados falsa para testar o DefaultTasksRepository.

  1. Clique com o botão direito do mouse no nome da classe DefaultTasksRepository, selecione Gerar e Testar.
  2. Siga as instruções para criar um DefaultTasksRepositoryTest no conjunto de origem test.
  3. Na parte superior da nova classe DefaultTasksRepositoryTest, adicione as variáveis de membro abaixo para representar as informações das suas fontes de dados falsas.

DefaultTasksRepositoryTest.kt (link em inglês)

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Crie três variáveis, duas variáveis de membro FakeDataSource (uma para cada fonte de dados do repositório) e uma variável para DefaultTasksRepository que será testada.

DefaultTasksRepositoryTest.kt (link em inglês)

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Criar um método para configurar e inicializar um DefaultTasksRepository testável. Este DefaultTasksRepository usará o teste duplo, FakeDataSource.

  1. Crie um método com o nome createRepository e adicione a anotação @Before.
  2. Instancie suas origens de dados falsas usando as listas remoteTasks e localTasks.
  3. Instancie o tasksRepository usando as duas fontes de dados falsas que você acabou de criar e a Dispatchers.Unconfined.

O método final ficará assim:

DefaultTasksRepositoryTest.kt (link em inglês)

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

Etapa 3: gravar o elemento TaskTasksRepository getTasks()

É hora de criar um teste de DefaultTasksRepository.

  1. Escreva um teste para o método getTasks do repositório. Verifique se, ao chamar getTasks com o true (o que significa que ele precisa ser atualizado da fonte de dados remota), ele retorna dados da fonte de dados remota, não da fonte local.

DefaultTasksRepositoryTest.kt (link em inglês)

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

Você verá um erro ao ligar para getTasks:

Etapa 4: adicionar runBlockingTest

O erro de corrotina é esperado porque getTasks é uma função suspend, e você precisa iniciar uma corrotina para chamá-la. Para isso, você precisa de um escopo de corrotina. Para resolver esse erro, você precisará adicionar algumas dependências do Gradle para processar a inicialização de corrotinas nos seus testes.

  1. Adicione as dependências necessárias para testar corrotinas ao conjunto de origem de teste usando testImplementation.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Não se esqueça de sincronizar.

kotlinx-coroutines-test é a biblioteca de testes de corrotinas, criada especificamente para testar corrotinas. Para executar os testes, use a função runBlockingTest. Esta é uma função fornecida pela biblioteca de testes de corrotinas. Ele usa um bloco de código e o executa em um contexto de corrotina especial que é executado de forma síncrona e imediata, ou seja, as ações ocorrerão em uma ordem determinística. Isso basicamente faz com que as corrotinas sejam executadas como não corrotinas, por isso serve para testar código.

Use runBlockingTest nas classes de teste ao chamar uma função suspend. Você aprenderá mais sobre como o runBlockingTest funciona e como testar corrotinas no próximo codelab desta série.

  1. Adicione o @ExperimentalCoroutinesApi acima da classe. Isso mostra que você sabe que está usando uma API de corrotinas experimental (runBlockingTest) na classe Sem isso, você receberá um aviso.
  2. De volta ao DefaultTasksRepositoryTest, adicione runBlockingTest para que ele receba todo o teste como um "bloco" de código

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

DefaultTasksRepositoryTest.kt (link em inglês)

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. Execute seu novo teste getTasks_requestsAllTasksFromRemoteDataSource e confirme se ele funciona e se o erro desapareceu.

Você acabou de ver como fazer um teste de unidade em um repositório. Nas próximas etapas, você usará novamente a injeção de dependência e criará outro teste duplo, desta vez para mostrar como criar testes de unidade e integração para seus modelos de visualização.

Os testes de unidade testam apenas a classe ou o método em que você tem interesse. Isso é conhecido como teste em isolamento, em que você isola claramente sua "unidade" e testa apenas o código que faz parte dessa unidade.

Portanto, o TasksViewModelTest precisa testar apenas o código do TasksViewModel, e não no banco de dados, na rede ou nas classes de repositório. Portanto, para seus modelos de visualização, assim como você fez para o repositório, crie um repositório fictício e aplique a injeção de dependência para usá-lo nos testes.

Nesta tarefa, você aplicará a injeção de dependência para visualizar modelos.

Etapa 1. Criar uma interface do TasksRepository

O primeiro passo para usar a injeção de dependência do construtor é criar uma interface comum compartilhada entre a classe falsa e a real.

Como isso funciona na prática? Analise TasksRemoteDataSource, TasksLocalDataSource e FakeDataSource e observe que todos compartilham a mesma interface: TasksDataSource. Isso permite que você diga no construtor da DefaultTasksRepository que você aceita em um TasksDataSource.

DefaultTasksRepository.kt (link em inglês)

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

É isso que nos permite fazer a troca no FakeDataSource.

Em seguida, crie uma interface para o DefaultTasksRepository, da mesma forma que você fez para as fontes de dados. Ela precisa incluir todos os métodos públicos (superfície da API pública) do DefaultTasksRepository.

  1. Abra DefaultTasksRepository e clique com o botão direito do mouse no nome da classe. Em seguida, selecione Refactor -> Extract -> Interface.

  1. Escolha Extrair para arquivo separado.

  1. Na janela Extract Interface, altere o nome da interface para TasksRepository.
  2. Na seção Interface "Membros para o formulário", marque todos os participantes exceto os dois participantes complementares e os métodos private.


  1. Clique em Refactor. A nova interface TasksRepository aparecerá no pacote data/source.

Agora, DefaultTasksRepository implementa TasksRepository.

  1. Execute o app (não os testes) para garantir que tudo ainda esteja funcionando.

Etapa 2: Criar FakeTestRepository

Agora que você tem a interface, pode criar o teste DefaultTaskRepository duplo.

  1. No conjunto de origem test, em data/source, crie o arquivo e a classe FakeTestRepository.kt do Kotlin e estenda a partir da interface TasksRepository.

FakeTestRepository.kt (link em inglês)

class FakeTestRepository : TasksRepository  {
}

Você receberá uma mensagem de que precisa implementar os métodos de interface.

  1. Passe o cursor sobre o erro até ver o menu de sugestões, clique e selecione Implementar membros.
  1. Selecione todos os métodos e pressione OK.

Etapa 3. Implementar métodos FakeTestRepository

Agora você tem uma classe FakeTestRepository com métodos "não implementados" Assim como você implementou a FakeDataSource, a FakeTestRepository terá uma estrutura de dados em vez de uma mediação complicada entre fontes locais e remotas.

Observe que o FakeTestRepository não precisa usar FakeDataSources nem nada assim. Ele só precisa retornar saídas falsas realistas de acordo com as entradas. Você usará um LinkedHashMap para armazenar a lista de tarefas e uma MutableLiveData para tarefas observáveis.

  1. No FakeTestRepository, adicione uma variável LinkedHashMap que represente a lista atual de tarefas e um MutableLiveData para as tarefas observáveis.

FakeTestRepository.kt (link em inglês)

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

Implemente os seguintes métodos:

  1. getTasks: esse método precisa transformar o tasksServiceData em uma lista usando tasksServiceData.values.toList() e depois retorná-lo como um resultado Success.
  2. refreshTasks: atualiza o valor de observableTasks para que seja retornado por getTasks().
  3. observeTasks: cria uma corrotina usando runBlocking, executa refreshTasks e retorna observableTasks.

Veja abaixo o código desses métodos.

FakeTestRepository.kt (link em inglês)

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

Etapa 4. Adição de um método para teste a addTasks.

Ao testar, é melhor ter alguns Tasks no seu repositório. Você pode chamar saveTask várias vezes, mas, para facilitar esse processo, adicione um método auxiliar especificamente para testes que permitem adicionar tarefas.

  1. Adicione o método addTasks, que recebe uma vararg de tarefas, adiciona cada uma às HashMap e atualiza as tarefas.

FakeTestRepository.kt (link em inglês)

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

Neste ponto, você tem um repositório fictício para testar com alguns dos principais métodos implementados. Em seguida, use isso nos testes.

Nesta tarefa, você usará uma classe falsa dentro de uma ViewModel. Use a injeção de dependência do construtor para adicionar as duas fontes de dados com a injeção de dependência do construtor adicionando uma variável TasksRepository ao construtor do TasksViewModel.

Esse processo é um pouco diferente dos modelos de visualização porque eles não são criados diretamente. Por exemplo:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


Como no código acima, você está usando a delegação de propriedade do viewModel's que cria o modelo de visualização. Para mudar a forma como o modelo de visualização é construído, é necessário adicionar e usar uma ViewModelProvider.Factory. Se você não conhece ViewModelProvider.Factory, saiba mais sobre ele.

Etapa 1. Criar e usar um ViewModelFactory no TasksViewModel

Comece atualizando as classes e os testes relacionados à tela Tasks.

  1. Abra TasksViewModel.
  2. Mude o construtor da TasksViewModel para receber TasksRepository em vez de construí-lo dentro da classe.

TasksViewModel.kt.

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

Como você alterou o construtor, agora precisa usar uma fábrica para construir TasksViewModel. Coloque a classe de fábrica no mesmo arquivo que o TasksViewModel, mas você também pode colocá-la no próprio arquivo.

  1. Na parte inferior do arquivo TasksViewModel, fora da classe, adicione uma TasksViewModelFactory que aceita um TasksRepository simples.

TasksViewModel.kt.

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


Esta é a maneira padrão de mudar a forma como ViewModels são construídos. Agora que você tem a fábrica, use-a sempre que criar seu modelo de visualização.

  1. Atualize o TasksFragment para usar a fábrica.

TasksFragment.kt.

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Execute o código do app e confira se tudo ainda está funcionando.

Etapa 2: Usar FakeTestRepository no TasksViewModelTest

Agora, em vez de usar o repositório real nos testes de modelos de visualização, você pode usar o repositório fictício.

  1. Abra o TasksViewModelTest.
  2. Adicione uma propriedade FakeTestRepository ao TasksViewModelTest.

TaskViewModelTest.kt.

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. Atualize o método setupViewModel para criar um FakeTestRepository com três tarefas e, em seguida, construa o tasksViewModel com esse repositório.

TasksViewModelTest.kt (link em inglês)

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. Como você não está mais usando o código do AndroidX Test ApplicationProvider.getApplicationContext, também é possível remover a anotação @RunWith(AndroidJUnit4::class).
  2. Execute seus testes e confira se todos eles ainda funcionam.

Ao usar a injeção de dependência do construtor, você removeu o DefaultTasksRepository como uma dependência e o substituiu pela sua FakeTestRepository nos testes.

Etapa 3. Atualizar também o Fragment TaskDetail e o ViewModel

Faça as mesmas mudanças em TaskDetailFragment e TaskDetailViewModel. Isso preparará o código para quando você criar testes do TaskDetail em seguida.

  1. Abra TaskDetailViewModel.
  2. Atualize o construtor:

TaskDetailViewModel.kt.

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. Na parte inferior do arquivo TaskDetailViewModel, fora da classe, adicione um TaskDetailViewModelFactory.

TaskDetailViewModel.kt.

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. Atualize o TasksFragment para usar a fábrica.

TasksFragment.kt.

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Execute o código e confira se tudo está funcionando.

Agora é possível usar um FakeTestRepository em vez do repositório real em TasksFragment e TasksDetailFragment

Em seguida, você criará testes de integração para testar o fragmento e as interações de modelo de visualização. Você descobrirá se o código do modelo de visualização atualiza a IU corretamente. Para fazer isso, você usa

  • o padrão ServiceLocator
  • as bibliotecas Espresso e Mockito

Os testes de integração analisam as interações de várias classes para verificar se elas se comportam conforme o esperado quando usadas juntas. Esses testes podem ser executados localmente (conjunto de origem test) ou como testes de instrumentação (conjunto de origem androidTest).

No seu caso, você usará cada fragmento e criará testes de integração para o fragmento e o modelo de visualização para testar os principais recursos.

Etapa 1. Adicionar dependências do Gradle

  1. Adicione as dependências do Gradle a seguir.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

Essas dependências incluem o seguinte:

  • junit:junit: JUnit, que é necessário para escrever instruções de teste básicas.
  • androidx.test:core: biblioteca de teste principal do AndroidX
  • kotlinx-coroutines-test: a biblioteca de testes de corrotinas
  • androidx.fragment:fragment-testing: biblioteca de teste AndroidX para criar fragmentos em testes e mudar seu estado.

Como você usará essas bibliotecas no seu conjunto de origem androidTest, use androidTestImplementation para adicioná-las como dependências.

Etapa 2: Criar uma classe TaskDetailFragmentTest

O TaskDetailFragment mostra informações sobre uma única tarefa.

Você começará escrevendo um teste de fragmento para o TaskDetailFragment, já que ele tem as funcionalidades básicas em comparação com os outros fragmentos.

  1. Abra taskdetail.TaskDetailFragment.
  2. Gere um teste de TaskDetailFragment, como você fez anteriormente. Aceite as opções padrão e coloque-as no conjunto de origem androidTest (NÃO no conjunto de origem de test).

  1. Adicione as seguintes anotações à classe TaskDetailFragmentTest.

TaskDetailFragmentTest.kt.

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

O objetivo dessa anotação é:

  • @MediumTest: marca o teste como um "teste de integração" de tempo médio (em vez de testes de unidade do @SmallTest e @LargeTest grandes testes de ponta a ponta). Isso ajuda você a agrupar e escolher o tamanho do teste a ser executado.
  • @RunWith(AndroidJUnit4::class): usado em qualquer classe com o AndroidX Test.

Etapa 3. Iniciar um fragmento de um teste

Nesta tarefa, você iniciará o TaskDetailFragment usando a biblioteca AndroidX Test. FragmentScenario é uma classe do AndroidX Test que envolve um fragmento e oferece controle direto sobre o ciclo de vida do fragmento para testes. Para programar testes de fragmentos, crie uma classe FragmentScenario para o fragmento que você está testando (TaskDetailFragment).

  1. Copie este teste para TaskDetailFragmentTest.

TaskDetailFragmentTest.kt.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

O código acima:

Este teste ainda não foi concluído, porque não declara nada. Por enquanto, execute o teste e veja o que acontece.

  1. Esse é um teste instrumentado. Portanto, confira se o emulador ou seu dispositivo está visível.
  2. Execute o teste.

Algumas coisas acontecerão.

  • Primeiro, como esse é um teste de instrumentação, ele será executado no dispositivo físico (se conectado) ou em um emulador.
  • Ele iniciará o fragmento.
  • Observe como ele não navega por nenhum outro fragmento nem tem menus associados à atividade. É apenas o fragmento.

Por fim, observe atentamente e observe que o fragmento diz "Não há dados", já que ele não carrega os dados da tarefa com sucesso.

O teste precisa carregar o TaskDetailFragment (que você fez) e declarar que os dados foram carregados corretamente. Por que não há dados? Isso ocorreu porque você criou uma tarefa, mas não a salvou no repositório.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Você tem esse FakeTestRepository, mas precisa de uma maneira de substituir o repositório real pelo repositório falso do seu fragmento. Você fará isso a seguir.

Nesta tarefa, você fornecerá seu repositório fictício para seu fragmento usando um ServiceLocator. Isso permitirá que você grave seu fragmento e veja os testes de integração do modelo.

Não é possível usar a injeção de dependência do construtor aqui, como você fez anteriormente, quando precisava fornecer uma dependência ao modelo de visualização ou repositório. A injeção de dependência do construtor exige que você construa a classe. Fragmentos e atividades são exemplos de classes que você não cria e geralmente não tem acesso ao construtor.

Como você não constrói o fragmento, não é possível usar a injeção de dependência do construtor para trocar o teste do repositório duplo (FakeTestRepository) pelo fragmento. Em vez disso, use o padrão Service Locator. O padrão do localizador de serviços é uma alternativa à injeção de dependência. Isso envolve a criação de uma classe Singleton chamada "Service Locator", com o objetivo de fornecer dependências para o código normal e de teste. No código normal do app (o conjunto de origem main), todas essas dependências são as dependências regulares do app. Para os testes, modifique o localizador de serviço para fornecer versões duplas de teste das dependências.

Não usa o localizador de serviços


Como usar um localizador de serviços

Para este app de codelab, faça o seguinte:

  1. Crie uma classe de localizador de serviços capaz de construir e armazenar um repositório. Por padrão, ele cria um repositório "normal" .
  2. Refatorar o código para usar o Localizador de serviços quando necessário.
  3. Na classe de teste, chame um método no Service Locator, que troca o repositório "normal" pelo duplo de teste.

Etapa 1. Criar o ServiceLocator

Vamos criar uma classe ServiceLocator. Ele será inserido no conjunto de origem principal com o restante do código do app porque é usado pelo código do aplicativo principal.

Observação: o ServiceLocator é um Singleton. Portanto, use a palavra-chave object do Kotlin (link em inglês) para a classe.

  1. Crie o arquivo ServiceLocator.kt no nível superior do conjunto de origem principal.
  2. Defina um object com o nome ServiceLocator.
  3. Crie as variáveis de instância database e repository e defina ambas como null.
  4. Anote o repositório com @Volatile, porque ele pode ser usado por várias linhas de execução (@Volatile é explicado em detalhes aqui).

O código ficará assim:

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

No momento, a única coisa que o ServiceLocator precisa fazer é saber como retornar um TasksRepository. Ele retornará um DefaultTasksRepository preexistente ou criará e retornará um novo DefaultTasksRepository, se necessário.

Defina as seguintes funções:

  1. provideTasksRepository: um repositório já existente ou um novo é criado. Esse método precisa ser synchronized em this para evitar, em situações com várias linhas de execução em execução, acidentalmente criando duas instâncias de repositório.
  2. createTasksRepository: código para criar um novo repositório. Ele chamará createTaskLocalDataSource e criará um novo TasksRemoteDataSource.
  3. createTaskLocalDataSource: código para criar uma nova fonte de dados local. Chamará createDataBase.
  4. createDataBase: código para criar um novo banco de dados.

Veja a seguir o código completo.

ServiceLocator.kt (em inglês)

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

Etapa 2: Usar o ServiceLocator no aplicativo

Você fará uma alteração no código principal do seu aplicativo (não nos testes) para criar o repositório em um só lugar, o ServiceLocator.

É importante que você crie apenas uma instância da classe de repositório. Para garantir isso, você usará o localizador de serviço na minha classe de aplicativo.

  1. No nível superior da hierarquia do pacote, abra TodoApplication e crie um val para o repositório e atribua a ele um repositório recebido usando ServiceLocator.provideTaskRepository.

TodoApplication.kt.

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Agora que você criou um repositório no aplicativo, é possível remover o antigo método getRepository no DefaultTasksRepository.

  1. Abra DefaultTasksRepository e exclua o objeto complementar.

DefaultTasksRepository.kt (link em inglês)

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

Agora, em qualquer lugar em que você usava getRepository, use o taskRepository do aplicativo. Isso garante que, em vez de fazer o repositório diretamente, você receba o repositório fornecido pelo ServiceLocator.

  1. Abra TaskDetailFragement e encontre a chamada para getRepository na parte superior da classe.
  2. Substitua essa chamada por uma que receba o repositório de TodoApplication.

TaskDetailFragment.kt.

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. Faça o mesmo para TasksFragment.

TasksFragment.kt.

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. Para StatisticsViewModel e AddEditTaskViewModel, atualize o código que adquire o repositório para usar o repositório do TodoApplication.

TasksFragment.kt.

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Execute o aplicativo (não o teste).

Como você só refatorou o app, ele deve ser executado sem problemas.

Etapa 3. Criar FakeAndroidTestRepository

Você já tem um FakeTestRepository no conjunto de origem de teste. Por padrão, não é possível compartilhar classes de teste entre os conjuntos de origem test e androidTest. Portanto, você precisa criar uma classe FakeTestRepository duplicada no conjunto de origem androidTest e chamá-la de FakeAndroidTestRepository.

  1. Clique com o botão direito do mouse no conjunto de origem androidTest e crie um pacote de dados. Clique com o botão direito novamente e crie um pacote source.
  2. Crie uma nova classe neste pacote de origem com o nome FakeAndroidTestRepository.kt.
  3. Copie o seguinte código para essa classe.

FakeAndroidTestRepository.kt (link em inglês)

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

Etapa 4. Preparar seu ServiceLocator para testes

Ok, é hora de usar a ServiceLocator para trocar o dobro no teste. Para fazer isso, você precisa adicionar um código ao ServiceLocator.

  1. Abra ServiceLocator.kt.
  2. Marque o setter para tasksRepository como @VisibleForTesting. Essa anotação é uma maneira de expressar que o motivo da razão pública é o teste.

ServiceLocator.kt (em inglês)

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Não importa se você executa o teste sozinho ou em um grupo de testes, eles precisam ser executados exatamente da mesma forma. Isso significa que os seus testes não devem ter nenhum comportamento dependente um do outro (o que significa evitar o compartilhamento de objetos entre testes).

Como a ServiceLocator é um Singleton, ela pode ser compartilhada acidentalmente entre os testes. Para ajudar a evitar isso, crie um método que redefina corretamente o estado ServiceLocator entre os testes.

  1. Adicione uma variável de instância chamada lock com o valor Any.

ServiceLocator.kt (em inglês)

private val lock = Any()
  1. Adicione um método específico de teste chamado resetRepository, que limpa o banco de dados e define o repositório e o banco de dados como nulo.

ServiceLocator.kt (em inglês)

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

Etapa 5: Usar o ServiceLocator

Nesta etapa, você usa o ServiceLocator.

  1. Abra TaskDetailFragmentTest.
  2. Declare uma variável lateinit TasksRepository.
  3. Adicione uma configuração e um método de desmontagem para configurar uma FakeAndroidTestRepository antes de cada teste e limpá-la após cada teste.

TaskDetailFragmentTest.kt.

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Una o corpo da função de activeTaskDetails_DisplayedInUi() em runBlockingTest.
  2. Salve activeTask no repositório antes de iniciar o fragmento.
repository.saveTask(activeTask)

O teste final ficará assim:

TaskDetailFragmentTest.kt.

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. Adicione a anotação @ExperimentalCoroutinesApi à classe inteira.

Quando terminar, o código ficará assim.

TaskDetailFragmentTest.kt.

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. Execute o teste activeTaskDetails_DisplayedInUi().

Assim como antes, você verá o fragmento, exceto que, agora que configurou o repositório corretamente, ele mostrará as informações da tarefa.


Nesta etapa, você usará a biblioteca de testes da IU do Espresso para concluir seu primeiro teste de integração. Você estruturau o código para poder adicionar testes com declarações para a IU. Para fazer isso, você usará a biblioteca de testes Espresso.

O Espresso ajuda você a:

  • Interaja com visualizações, como clicar em botões, deslizar uma barra ou rolar uma tela para baixo.
  • Declarar que determinadas visualizações estão na tela ou estão em determinado estado (por exemplo, com texto específico ou com uma caixa de seleção marcada etc.)

Etapa 1. Observação da dependência do Gradle

Você já terá a dependência principal do Espresso, já que ela é incluída em projetos Android por padrão.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core: essa dependência principal do Espresso é incluída por padrão quando você cria um novo projeto Android. Ele contém o código de teste básico para a maioria das visualizações e ações relacionadas.

Etapa 2: Desativar animações

Os testes do Espresso são executados em um dispositivo real e, portanto, são testes de instrumentação por natureza. Um problema que acontece são as animações: se uma animação trava e você tenta testar se uma visualização está na tela, mas ela ainda está sendo animada, o Espresso pode falhar acidentalmente em um teste. Isso pode tornar os testes do Espresso instáveis.

Para os testes de IU do Espresso, é recomendável desativar as animações. O teste será executado mais rapidamente.

  1. No dispositivo de teste, acesse Configurações > Opções do desenvolvedor.
  2. Desative estas três configurações: Escala de animação da janela, Escala de animação da transição e Escala de duração do animador.

Etapa 3. Ver um teste do Espresso

Antes de programar um teste do Espresso, confira alguns códigos do Espresso.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

Essa instrução encontra a visualização da caixa de seleção com o ID task_detail_complete_checkbox, clica nela e declara que está marcada.

A maioria das instruções do Espresso são compostas por quatro partes:

1. Método estático do Espresso

onView

onView é um exemplo de um método estático do Espresso que inicia uma instrução do Espresso. onView é uma das mais comuns, mas existem outras opções, como onData.

2. ViewMatcher.

withId(R.id.task_detail_title_text)

withId é um exemplo de ViewMatcher que recebe uma visualização pelo ID. Há outros correspondentes de visualização que podem ser consultados na documentação.

3. ViewAction

perform(click())

O método perform que usa um ViewAction. Um ViewAction pode ser usado para visualizar a página, por exemplo, quando ele clica na visualização.

4. ViewAssertion.

check(matches(isChecked()))

check, que usa um ViewAssertion. ViewAssertions verificam ou declaram algo sobre a visualização. A ViewAssertion mais comum que você usará é a declaração matches. Para concluir a declaração, use outro ViewMatcher, neste caso isChecked.

Observe que você nem sempre chama perform e check em uma instrução Espresso. É possível ter instruções que apenas fazem uma declaração usando check ou apenas um ViewAction usando perform.

  1. Abra TaskDetailFragmentTest.kt.
  2. Atualize o teste activeTaskDetails_DisplayedInUi.

TaskDetailFragmentTest.kt.

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Veja as instruções de importação, se necessário:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. Tudo o que aparece depois do comentário // THEN usa o Espresso. Analise a estrutura de teste e o uso de withId e confira se há declarações sobre a aparência da página de detalhes.
  2. Execute o teste e confirme se ele foi aprovado.

Etapa 4. Opcional: criar seu próprio teste do Espresso

Agora, crie um teste.

  1. Crie um novo teste chamado completedTaskDetails_DisplayedInUi e copie esse código esqueleto.

TaskDetailFragmentTest.kt.

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. Analisando o teste anterior, conclua este teste.
  2. Execute e confirme a aprovação do teste.

A completedTaskDetails_DisplayedInUi concluída ficará parecida com este código.

TaskDetailFragmentTest.kt.

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

Na última etapa, você aprenderá a testar o componente de navegação usando um tipo diferente de teste duplo chamado simulação e a biblioteca de teste Mockito.

Neste codelab, você usou um teste duplo chamado falso. As falsificações são um dos muitos tipos de cópias de teste. Qual teste duplo você deve usar para testar o componente de navegação?

Pense em como a navegação acontece. Imagine pressionar uma das tarefas no TasksFragment para navegar até uma tela de detalhes da tarefa.

Este código em TasksFragment, que navega para uma tela detalhada de tarefas quando é pressionado.

TasksFragment.kt.

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


A navegação ocorre devido a uma chamada para o método navigate. Caso seja necessário escrever uma declaração de declaração, não há uma maneira direta de testar se você navegou para TaskDetailFragment. Navegar é uma ação complicada que não resulta em uma saída ou mudança de estado clara, além de inicializar TaskDetailFragment.

É possível declarar que o método navigate foi chamado com o parâmetro de ação correto. Isso é exatamente o que um teste de simulação faz e verifica se métodos específicos foram chamados.

O Mockito é um framework para criar testes duplos. Embora a palavra simulação seja usada na API e no nome, ela não serve para fazer simulações. Também pode criar stubs e espiões.

Você usará o Mockito para criar uma NavigationController fictícia, que pode declarar que o método de navegação foi chamado corretamente.

Etapa 1. Adicionar dependências do Gradle

  1. Adicione as dependências do Gradle.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core: é a dependência do Mockito.
  • dexmaker-mockito: esta biblioteca é necessária para usar o Mockito em um projeto Android. O Mockito precisa gerar classes no momento da execução. No Android, isso é feito usando o código de bytes dex. Portanto, essa biblioteca permite que o Mockito gere objetos durante o tempo de execução no Android.
  • androidx.test.espresso:espresso-contrib: essa biblioteca é composta por contribuições externas, que têm o nome de um código de teste para visualizações mais avançadas, como DatePicker e RecyclerView. Ele também contém as verificações de acessibilidade e a classe CountingIdlingResource, que serão abordadas mais tarde.

Etapa 2: Criar TasksFragmentTest

  1. Abra o TasksFragment
  2. Clique com o botão direito do mouse no nome da classe TasksFragment e selecione Gerar e Testar. Crie um teste no conjunto de origem androidTest.
  3. Copie esse código para o TasksFragmentTest.

TasksFragmentTest.kt.

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

Esse código é semelhante ao código TaskDetailFragmentTest que você escreveu. Ele configura e desmonta um FakeAndroidTestRepository. Adicione um teste de navegação para verificar se, ao clicar em uma tarefa na lista de tarefas, você acessa o TaskDetailFragment correto.

  1. Adicione o teste clickTask_navigateToDetailFragmentOne.

TasksFragmentTest.kt.

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Use a função mock do Mockitoc para criar uma simulação.

TasksFragmentTest.kt.

 val navController = mock(NavController::class.java)

Para simular no Mockito, transmita a classe que você quer simular.

Em seguida, você precisa associar a NavController ao fragmento. onFragment permite que você chame métodos no próprio fragmento.

  1. Faça a nova simulação do NavController do fragmento.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Adicione o código para clicar no item no RecyclerView que tem o texto"quot;TITLE1"".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

O RecyclerViewActions faz parte da biblioteca espresso-contrib e permite executar ações do Espresso em um RecyclerView.

  1. Verifique se navigate foi chamado com o argumento correto.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

O método verify do Mockito gera o teste. Você pode confirmar quando um navController simulado é chamado de um método específico (navigate) com um parâmetro (actionTasksFragmentToTaskDetailFragment com o ID de "id1").

O teste completo fica assim:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Execute o teste.

Em resumo, para testar a navegação, você pode:

  1. Usar o Mockito para criar uma simulação NavController
  2. Anexe essa NavController simulada ao fragmento.
  3. Verifique se a navegação foi chamada com a ação e os parâmetros corretos.

Etapa 3. Opcional: escreva clickAddTaskButton_navigationToAddEditFragment

Para ver se você pode criar um teste de navegação, tente realizar esta tarefa.

  1. Escreva o teste clickAddTaskButton_navigateToAddEditFragment, que verifica se você clica no + FAB e navega para o AddEditTaskFragment.

A resposta está abaixo.

TasksFragmentTest.kt.

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

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_2


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 ensinou como configurar a injeção de dependência manual, um localizador de serviços e como usar simulações e simulações nos seus apps Kotlin para Android. Especificamente, as seguintes:

  • O que você quer testar e a estratégia de teste determina os tipos de teste que você implementará no app. Os testes de unidade são focados e rápidos. Os testes de integração confirmam a interação entre partes do programa. Os testes completos verificam os recursos, têm a maior fidelidade, costumam ser instrumentados e podem demorar mais para serem executados.
  • A arquitetura do app influencia a dificuldade de teste.
  • TDD ou desenvolvimento orientado por testes é uma estratégia em que você programa os testes primeiro e depois cria o recurso para passar nos testes.
  • Para isolar partes do app para testes, use testes duplos. Um teste duplo é uma versão de uma classe criada especificamente para testes. Por exemplo, você finge receber dados de um banco de dados ou da Internet.
  • Use a injeção de dependência para substituir uma classe real por uma classe de teste, por exemplo, um repositório ou uma camada de rede.
  • Use testes ilustrados (androidTest) para iniciar os componentes da IU.
  • Quando não é possível usar a injeção de dependência do construtor, por exemplo, para iniciar um fragmento, você pode usar um localizador de serviços. O padrão do localizador de serviços é uma alternativa à injeção de dependência. Isso envolve a criação de uma classe Singleton chamada "Service Locator", com o objetivo de fornecer dependências para o código normal e de teste.

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.