Introdução a duplicação de testes e injeção de dependência

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

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

  • Testes de unidade do repositório
  • Testes de integração de fragmentos e 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 teste
  • Como criar e usar cópias de teste, ou seja, fakes e mocks
  • Como usar a injeção manual de dependência no Android para testes de unidade e integração
  • Como aplicar o padrão Service Locator
  • Como testar repositórios, fragmentos, ViewModels e o componente Navigation

Você vai usar as seguintes bibliotecas e conceitos de programação:

Atividades deste laboratório

  • Escrever testes de unidade para um repositório usando um double de teste e injeção de dependência.
  • Escrever testes de unidade para um modelo de visualização usando um double de teste e injeção de dependência.
  • Escreva testes de integração para fragmentos e viewmodels usando o framework de testes de UI do Espresso.
  • Escrever testes de navegação usando Mockito e Espresso.

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.

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

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:

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

Neste codelab, você vai aprender a testar repositórios, modelos de visualização e fragmentos usando substitutos de teste e injeção de dependência. Antes de saber quais são, é importante entender o raciocínio que vai orientar o que e como você vai escrever esses testes.

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

A pirâmide de testes

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

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

Há compensações inerentes entre esses aspectos. Por exemplo, velocidade e fidelidade são uma troca: quanto mais rápido o teste, geralmente, menor a fidelidade e vice-versa. Uma maneira comum de dividir os testes automatizados é em três categorias:

  • Testes de unidade: são testes altamente focados que são executados em uma única classe, geralmente um único método nessa classe. Se um teste de unidade falhar, você saberá exatamente onde está o problema no seu código. Eles têm baixa fidelidade, já que, no mundo real, seu app envolve muito mais do que a execução de um método ou classe. Eles são rápidos o suficiente para serem executados sempre que você muda o código. Na maioria das vezes, eles são testes executados localmente (no conjunto de origem test). Exemplo : 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 é testar um único recurso, como a capacidade de salvar uma tarefa. Eles testam um escopo maior de código do que os testes de unidade, mas ainda são otimizados para serem executados rapidamente, em vez de ter fidelidade total. Eles podem ser executados localmente ou como testes de instrumentação, dependendo da situação. Exemplo : testar toda a funcionalidade de um único par de fragmento e modelo de visualização.
  • Testes de ponta a ponta (E2e): testam uma combinação de recursos funcionando juntos. Eles testam grandes partes do app, simulam o uso real de perto e, portanto, costumam ser lentos. Eles têm a maior fidelidade e mostram que seu aplicativo funciona como um todo. Em geral, esses testes serão instrumentados (no conjunto de origem androidTest).
    Exemplo : iniciar todo o app e testar alguns recursos juntos.

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

Arquitetura e teste

Sua capacidade de testar o app em todos os diferentes níveis da pirâmide de testes está inerentemente vinculada à arquitetura do app. Por exemplo, um aplicativo extremamente mal arquitetado pode colocar toda a lógica em um método. Talvez seja possível escrever um teste de ponta a ponta para isso, já que esses testes tendem a testar grandes partes do app. Mas e os 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 os testes de unidade e de integração. O app de tarefas que você vai testar segue uma arquitetura específica:



Nesta lição, você vai aprender a testar partes da arquitetura acima, em isolamento adequado:

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

Os testes de ponta a ponta serão abordados na próxima aula.

Ao escrever um teste de unidade para uma parte de uma classe (um método ou uma pequena coleção de métodos), o objetivo é testar apenas o código dessa classe.

Testar apenas o código em uma ou mais classes específicas pode ser complicado. Vamos conferir um exemplo. Abra a classe data.source.DefaultTaskRepository no conjunto de origem main. Esse é o repositório do app e a classe para a qual você vai escrever testes de unidade em seguida.

Seu objetivo é testar apenas o código dessa classe. No entanto, DefaultTaskRepository depende de outras classes, como LocalTaskDataSource e RemoteTaskDataSource, para funcionar. Outra maneira de dizer isso é que LocalTaskDataSource e RemoteTaskDataSource são dependências de DefaultTaskRepository.

Assim, 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, confira 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 "básicas" que você pode fazer ao seu repositório. Esse método inclui a leitura de um banco de dados SQLite e a realização de chamadas de rede (a chamada para updateTasksFromRemoteDataSource). Isso envolve muito mais código do que apenas o código do repositório.

Confira alguns motivos mais específicos que dificultam o teste do repositório:

  • Você precisa pensar em criar e gerenciar um banco de dados para fazer até mesmo os testes mais simples para esse repositório. Isso levanta questões como "deve ser um teste local ou instrumentado?" e se você deve usar o AndroidX Test para ter um ambiente Android simulado.
  • Algumas partes do código, como o de rede, podem levar muito tempo para serem executadas ou até mesmo falhar ocasionalmente, criando testes de longa duração e instáveis.
  • Seus testes podem perder a capacidade de diagnosticar qual código é o culpado por uma falha. Seus testes podem começar a testar código que não é de repositório. Por exemplo, os supostos testes de unidade de "repositório" podem falhar devido a um problema em parte do código dependente, como o código do banco de dados.

Testes substitutos

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

Confira alguns tipos de substitutos de teste:

Falso

Um substituto de teste que tem uma implementação "funcional" da classe, mas é implementado de uma forma que o torna bom para testes, mas inadequado para produção.

Mock

Um double de teste que rastreia quais dos métodos foram chamados. Em seguida, ele aprova ou reprova um teste dependendo se os métodos foram chamados corretamente.

Stub

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

Dummy

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

Spy (link em inglês)

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

Para mais informações sobre substitutos de teste, confira Testing on the Toilet: Know Your Test Doubles (em inglês).

Os testes duplos mais comuns usados no Android são falsos e simulados.

Nesta tarefa, você vai criar um simulador de teste FakeDataSource para testar a unidade DefaultTasksRepository separada das fontes de dados reais.

Etapa 1: criar a classe FakeDataSource

Nesta etapa, você vai criar uma classe chamada FakeDataSouce, que será um double de teste de um LocalDataSource e um RemoteDataSource.

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

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

Etapa 2: implementar a interface TasksDataSource

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

  1. Observe como ambos 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 vai informar 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 em FakeDataSource

FakeDataSource é um tipo específico de double de teste chamado fake. Um falso é um substituto de teste que tem uma implementação "funcional" da classe, mas é implementado de uma forma que o torna bom para testes, mas inadequado para produção. Uma implementação "funcional" significa que a classe vai produzir saídas realistas com base nas entradas.

Por exemplo, sua 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 vai "funcionar como esperado", já que os métodos para receber ou salvar tarefas vão retornar os resultados esperados. No entanto, você nunca poderá usar essa implementação em produção, porque ela não é salva no servidor ou em um banco de dados.

Um FakeDataSource

  • permite testar o código em DefaultTasksRepository sem precisar depender de um banco de dados ou rede real.
  • fornece uma implementação "real o suficiente" para 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 "simulam" ser uma resposta de banco de dados ou servidor. Por enquanto, o objetivo é testar o método getTasks da classe repository. Isso chama os métodos getTasks, deleteAllTasks e saveTask da fonte de dados .

Escreva uma versão falsa destes métodos:

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

Esses métodos, implementados para FakeDataSource, são parecidos com o 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)
}

Confira 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 é semelhante ao funcionamento das fontes de dados locais e remotas reais.

Nesta etapa, você vai usar uma técnica chamada injeção de dependência manual para usar o falso duplo de teste 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 TasksRemoteDataSource e TasksLocalDataSource, mas apenas nos testes. TasksRemoteDataSource e TasksLocalDataSource são dependências de DefaultTasksRepository, ou seja, DefaultTasksRepositories exige ou "depende" dessas classes para ser executado.

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

DefaultTasksRepository.kt

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 de forma fixa. Não há como trocar por uma cópia de teste.

Em vez disso, forneça essas fontes de dados à classe, em vez de codificá-las. Fornecer dependências é conhecido como injeção de dependência. Há diferentes maneiras de fornecer dependências e, portanto, diferentes tipos de injeção de dependência.

Com a injeção de dependência do construtor, é possível trocar o double de teste transmitindo-o ao 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 receber as duas fontes de dados e o dispatcher de corrotina (que também precisa ser trocado nos testes. Isso é descrito com mais detalhes na terceira lição sobre corrotinas).Application

DefaultTasksRepository.kt

// 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 variáveis de instância antigas. Você os define no construtor:

DefaultTasksRepository.kt

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

    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 seu código está usando a injeção de dependência do construtor, você pode usar sua fonte de dados falsa para testar o DefaultTasksRepository.

  1. Clique com o botão direito do mouse no nome da classe DefaultTasksRepository e selecione Generate e Test.
  2. Siga as instruções para criar DefaultTasksRepositoryTest no conjunto de origem test.
  3. Na parte de cima da nova classe DefaultTasksRepositoryTest, adicione as variáveis de membro abaixo para representar os dados nas fontes de dados falsas.

DefaultTasksRepositoryTest.kt

    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 o DefaultTasksRepository que você vai testar.

DefaultTasksRepositoryTest.kt

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

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Crie um método para configurar e inicializar um DefaultTasksRepository testável. Esse DefaultTasksRepository vai usar sua dupla de teste, FakeDataSource.

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

O método final ficará assim:

DefaultTasksRepositoryTest.kt

    @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 teste DefaultTasksRepository getTasks()

É hora de escrever um teste DefaultTasksRepository!

  1. Crie um teste para o método getTasks do repositório. Verifique se, ao chamar getTasks com true (ou seja, ele deve recarregar da fonte de dados remota), ele retorna dados da fonte de dados remota (em vez da fonte de dados local).

DefaultTasksRepositoryTest.kt

@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ê vai receber um erro ao chamar 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 corrotinas. Para resolver esse erro, adicione algumas dependências do Gradle para processar o lançamento 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 teste de corrotinas, criada especificamente para testar corrotinas. Para executar os testes, use a função runBlockingTest. Essa é uma função fornecida pela biblioteca de testes de corrotinas. Ele recebe um bloco de código e o executa em um contexto especial de corrotina que é executado de forma síncrona e imediata. Isso significa que as ações vão ocorrer em uma ordem determinística. Isso faz com que suas corrotinas sejam executadas como não corrotinas, portanto, é destinado a testes de código.

Use runBlockingTest nas classes de teste ao chamar uma função suspend. Você vai 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 expressa que você sabe que está usando uma API de corrotina experimental (runBlockingTest) na classe. Sem ela, você vai receber um aviso.
  2. Volte para o DefaultTasksRepositoryTest e adicione runBlockingTest para que ele receba todo o teste como um "bloco" de código.

O teste final é parecido com o código abaixo.

DefaultTasksRepositoryTest.kt

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 o novo teste getTasks_requestsAllTasksFromRemoteDataSource e confirme se ele funciona e o erro desapareceu.

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

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

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

Nesta tarefa, você vai aplicar a injeção de dependência aos modelos de visualização.

Etapa 1. Criar uma interface TasksRepository

A primeira etapa 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? Observe que TasksRemoteDataSource, TasksLocalDataSource e FakeDataSource compartilham a mesma interface: TasksDataSource. Isso permite que você diga no construtor de DefaultTasksRepository que você recebe um TasksDataSource.

DefaultTasksRepository.kt

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

Isso permite que troquemos seu FakeDataSource.

Em seguida, crie uma interface para DefaultTasksRepository, como fez para as fontes de dados. Ele precisa incluir todos os métodos públicos (superfície pública da API) de 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 um arquivo separado.

  1. Na janela Extrair interface, mude o nome da interface para TasksRepository.
  2. Na seção Interface para formar participantes, marque todos os participantes exceto os dois participantes complementares e os métodos privados.


  1. Clique em Refatorar. A nova interface TasksRepository vai aparecer no pacote data/source .

E DefaultTasksRepository agora 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, é possível criar o simulador de teste DefaultTaskRepository.

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

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Você vai precisar implementar os métodos da interface.

  1. Passe o cursor sobre o erro até que o menu de sugestões apareça. Em seguida, 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 o FakeDataSource, o FakeTestRepository será apoiado por uma estrutura de dados, em vez de lidar com uma mediação complicada entre fontes de dados locais e remotas.

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

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

FakeTestRepository.kt

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 usar o tasksServiceData e transformá-lo em uma lista usando tasksServiceData.values.toList() e retornar isso como um resultado Success.
  2. refreshTasks: atualiza o valor de observableTasks para o que é retornado por getTasks().
  3. observeTasks: cria uma corrotina usando runBlocking e executa refreshTasks. Depois, retorna observableTasks.

Confira abaixo o código desses métodos.

FakeTestRepository.kt

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. Adicionar um método para teste a addTasks

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

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

FakeTestRepository.kt

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

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

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

Esse processo é um pouco diferente com modelos de visualização porque você não os cria diretamente. Por exemplo:

class TasksFragment : Fragment() {

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

}


Como no código acima, você está usando o delegado de propriedade viewModel's, que cria o modelo de visualização. Para mudar a forma como o modelo de visualização é construído, adicione e use um ViewModelProvider.Factory. Se você não conhece o ViewModelProvider.Factory, saiba mais neste link.

Etapa 1. Criar e usar uma ViewModelFactory em TasksViewModel

Comece atualizando as classes e o teste relacionados à tela Tasks.

  1. Abra TasksViewModel.
  2. Mude o construtor de TasksViewModel para usar 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ê mudou o construtor, agora é necessário usar uma fábrica para construir TasksViewModel. Coloque a classe de fábrica no mesmo arquivo que o TasksViewModel, mas também é possível colocar em um arquivo próprio.

  1. Na parte de baixo do arquivo TasksViewModel, fora da classe, adicione um TasksViewModelFactory que receba 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)
}


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

  1. Atualize 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 em TasksViewModelTest

Agora, em vez de usar o repositório real nos testes do modelo de visualização, você pode usar o repositório falso.

  1. Abra 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 construa o tasksViewModel com esse repositório.

TasksViewModelTest.kt

    @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 AndroidX Test ApplicationProvider.getApplicationContext, também é possível remover a anotação @RunWith(AndroidJUnit4::class).
  2. Execute os testes e confira se eles ainda funcionam.

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

Etapa 3. Também atualize o fragmento TaskDetail e o ViewModel

Faça as mesmas mudanças para o TaskDetailFragment e o TaskDetailViewModel. Isso vai preparar o código para quando você escrever os próximos testes TaskDetail.

  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 de baixo 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 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ê vai escrever testes de integração para testar as interações do fragmento e do modelo de visualização. Você vai descobrir se o código do modelo de visualização atualiza a interface corretamente. Para fazer isso, use

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

Os testes de integração testam a interação de várias classes para garantir que elas se comportem 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ê vai pegar cada fragmento e escrever testes de integração para o fragmento e o modelo de visualização para testar os principais recursos do fragmento.

Etapa 1. Adicionar dependências do Gradle

  1. Adicione as seguintes dependências do Gradle.

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:

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

Como você vai usar essas bibliotecas no 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ê vai começar escrevendo um teste de fragmento para o TaskDetailFragment, já que ele tem uma funcionalidade bastante básica em comparação com os outros fragmentos.

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

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

TaskDetailFragmentTest.kt

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

}

O objetivo dessas anotações é:

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

Etapa 3. Iniciar um fragmento de um teste

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

  1. Copie esse 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)

    }

Este código acima:

Esse ainda não é um teste concluído, porque não está declarando nada. Por enquanto, execute o teste e observe o que acontece.

  1. Este é um teste instrumentado. Portanto, o emulador ou dispositivo precisa estar visível.
  2. Execute o teste.

Algumas coisas vão acontecer.

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

Por fim, observe que o fragmento diz "Sem dados", já que não carrega os dados da tarefa.

Seu teste precisa carregar o TaskDetailFragment (o que você já fez) e confirmar se os dados foram carregados corretamente. Por que não há dados? Isso acontece 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 falso no fragmento. Você vai fazer isso a seguir.

Nesta tarefa, você vai fornecer seu repositório falso ao fragmento usando um ServiceLocator. Isso permite escrever testes de integração de fragmentos e modelos de visualização.

Não é possível usar a injeção de dependência do construtor aqui, como você fez antes, quando precisava fornecer uma dependência ao modelo de visualização ou ao 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 constrói e geralmente não tem acesso ao construtor.

Como você não cria o fragmento, não é possível usar a injeção de dependência do construtor para trocar o simulacro de teste do repositório (FakeTestRepository) pelo fragmento. Em vez disso, use o padrão Localizador de serviço. 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", cujo objetivo é fornecer dependências para o código regular e de teste. No código do app normal (o conjunto de origem main), todas essas dependências são as dependências normais do app. Para os testes, você modifica o localizador de serviços para fornecer versões de teste duplo das dependências.

Não usar o Service Locator


Como usar um localizador de serviços

Para o app deste codelab, faça o seguinte:

  1. Crie uma classe de localizador de serviço que possa construir e armazenar um repositório. Por padrão, ele cria um repositório "normal".
  2. Refatore o código para que, quando você precisar de um repositório, use o localizador de serviços.
  3. Na classe de teste, chame um método no localizador de serviços que substitui o repositório "normal" pelo seu double de teste.

Etapa 1. Criar o ServiceLocator

Vamos criar uma classe ServiceLocator. Ele vai ficar no conjunto de fontes principal com o restante do código do app porque é usado pelo código principal do aplicativo.

Observação: o ServiceLocator é um singleton. Portanto, use a palavra-chave object do Kotlin para a classe.

  1. Crie o arquivo ServiceLocator.kt no nível superior do conjunto de origem principal.
  2. Defina um object chamado ServiceLocator.
  3. Crie variáveis de instância database e repository e defina ambas como null.
  4. Anotar 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 vai ficar assim:

object ServiceLocator {

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

}

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

Defina as seguintes funções:

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

O código completo está abaixo.

ServiceLocator.kt

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 ServiceLocator no aplicativo

Você vai fazer uma mudança no código principal do 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ê vai usar o localizador de serviços na classe Application.

  1. No nível superior da hierarquia de pacotes, abra TodoApplication e crie um val para seu repositório. Em seguida, atribua a ele um repositório obtido 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, remova o método getRepository antigo em DefaultTasksRepository.

  1. Abra DefaultTasksRepository e exclua o objeto complementar.

DefaultTasksRepository.kt

// 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 vez de getRepository, use o taskRepository do aplicativo. Isso garante que, em vez de criar o repositório diretamente, você receba qualquer repositório fornecido pelo ServiceLocator.

  1. Abra TaskDetailFragement e encontre a chamada para getRepository na parte de cima 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ê apenas refatorou, o app deve ser executado da mesma forma sem problemas.

Etapa 3. Create FakeAndroidTestRepository

Você já tem um FakeTestRepository no conjunto de fontes de teste. Por padrão, não é possível compartilhar classes de teste entre os conjuntos de origem test e androidTest. Portanto, é necessário 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 do mouse novamente e crie um pacote de origem .
  2. Crie uma nova classe chamada FakeAndroidTestRepository.kt nesse pacote de origem.
  3. Copie o código a seguir para essa classe.

FakeAndroidTestRepository.kt

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 o ServiceLocator para testes

Ok, agora é hora de usar o ServiceLocator para trocar por testes duplos ao testar. Para isso, adicione um código ao seu ServiceLocator.

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

ServiceLocator.kt

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

Seja executado sozinho ou em um grupo, o teste precisa ser executado exatamente da mesma forma. Isso significa que seus testes não podem ter comportamentos dependentes uns dos outros, o que significa evitar o compartilhamento de objetos entre testes.

Como o ServiceLocator é um singleton, ele pode ser compartilhado acidentalmente entre testes. Para 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

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

ServiceLocator.kt

    @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 seu ServiceLocator

Nesta etapa, você vai usar o ServiceLocator.

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

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

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

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

O teste final é semelhante ao código abaixo.

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 vai 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, o fragmento vai aparecer, mas desta vez, como você configurou o repositório corretamente, ele vai mostrar as informações da tarefa.


Nesta etapa, você vai usar a biblioteca de testes de interface do Espresso para concluir seu primeiro teste de integração. Você estruturou seu código para poder adicionar testes com asserções para sua interface. Para isso, use a biblioteca de testes do Espresso.

O Espresso ajuda você a:

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

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

Você já tem a principal dependência do Espresso, porque ela é incluída nos projetos do 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 do Android. Ele contém o código de teste básico para a maioria das visualizações e ações nelas.

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 surge são as animações: se uma animação ficar lenta e você tentar testar se uma visualização está na tela, mas ela ainda estiver sendo animada, o Espresso poderá falhar em um teste por acidente. Isso pode tornar os testes do Espresso instáveis.

Para testes de IU do Espresso, a prática recomendada é desativar as animações (assim, o teste será executado mais rápido):

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

Etapa 3. Analisar um teste do Espresso

Antes de escrever um teste do Espresso, confira um pouco do código 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 afirma que ela está marcada.

A maioria das instruções do Espresso é composta por quatro partes:

1. Método estático do Espresso

onView

onView é um exemplo de método estático do Espresso que inicia uma instrução do Espresso. onView é uma das mais comuns, mas há 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. Uma ViewAction é algo que pode ser feito na visualização. Por exemplo, aqui, é clicar na visualização.

4. ViewAssertion

check(matches(isChecked()))

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

Nem sempre é necessário chamar perform e check em uma instrução do Espresso. Você pode ter instruções que apenas fazem uma declaração usando check ou apenas fazem 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())))
    }

Confira 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. Examine a estrutura do teste e o uso de withId e verifique se há declarações sobre como a página de detalhes deve aparecer.
  2. Execute o teste e confirme se ele foi aprovado.

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

Agora escreva um teste.

  1. Crie um novo teste chamado completedTaskDetails_DisplayedInUi e copie este código de estrutura.

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. Com base no teste anterior, conclua este teste.
  2. Execute e confirme se o teste foi aprovado.

O completedTaskDetails_DisplayedInUi finalizado vai ficar parecido 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()))
    }

Nesta última etapa, você vai aprender a testar o componente Navigation usando um tipo diferente de simulador de teste chamado mock e a biblioteca de testes Mockito.

Neste codelab, você usou um double de teste chamado fake. Os fakes são um dos muitos tipos de simuladores de teste. Qual substituto de teste você deve usar para testar o componente Navigation?

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

Confira o código em TasksFragment que navega para uma tela de detalhes da tarefa quando ela é pressionada.

TasksFragment.kt

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


A navegação ocorre devido a uma chamada ao método navigate. Se você precisasse escrever uma instrução de asserção, não haveria uma maneira simples de testar se você navegou até TaskDetailFragment. A navegação é uma ação complicada que não resulta em uma saída clara ou mudança de estado, além de inicializar TaskDetailFragment.

O que você pode afirmar é que o método navigate foi chamado com o parâmetro de ação correto. É exatamente isso que um mock faz: ele verifica se métodos específicos foram chamados.

O Mockito (link em inglês) é um framework para criar testes duplos. Embora a palavra "mock" seja usada na API e no nome, ela não serve apenas para criar simulações. Ele também pode criar stubs e spies.

Você vai usar o Mockito para criar um NavigationController simulado que pode afirmar 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: essa é a dependência do Mockito.
  • dexmaker-mockito: essa biblioteca é necessária para usar o Mockito em um projeto Android. O Mockito precisa gerar classes em tempo de execução. No Android, isso é feito usando bytecode dex. Assim, essa biblioteca permite que o Mockito gere objetos durante o tempo de execução no Android.
  • androidx.test.espresso:espresso-contrib: essa biblioteca é composta de contribuições externas (por isso o nome) que contêm código de teste para visualizações mais avançadas, como DatePicker e RecyclerView. Ele também contém verificações de acessibilidade e uma classe chamada CountingIdlingResource, que será abordada mais adiante.

Etapa 2. Criar TasksFragmentTest

  1. Abra TasksFragment.
  2. Clique com o botão direito do mouse no nome da classe TasksFragment e selecione Generate e Test. 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 encerra um FakeAndroidTestRepository. Adicione um teste de navegação para verificar se, ao clicar em uma tarefa na lista, você é direcionado para 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 Mockito para criar um simulacro.

TasksFragmentTest.kt

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

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

Em seguida, associe o NavController ao fragmento. Com onFragment, é possível chamar métodos no próprio fragmento.

  1. Faça com que o novo simulacro seja o 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 "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 realizar 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 é o que faz isso ser um simulacro. É possível confirmar que o navController simulado chamou um método específico (navigate) com um parâmetro (actionTasksFragmentToTaskDetailFragment com o ID "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. Use o Mockito para criar um simulacro NavController.
  2. Anexe o NavController simulado ao fragmento.
  3. Verifique se "navigate" foi chamado com a ação e os parâmetros corretos.

Etapa 3. Opcional, escreva clickAddTaskButton_navigateToAddEditFragment

Para saber se você consegue escrever um teste de navegação, tente esta tarefa.

  1. Escreva o teste clickAddTaskButton_navigateToAddEditFragment, que verifica se, ao clicar no FAB de adição, você navega até 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 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_2


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

  • O que você quer testar e sua estratégia de teste determinam os tipos de teste que você vai implementar no app. Os testes de unidade são focados e rápidos. Os testes de integração verificam a interação entre partes do programa. Os testes de ponta a ponta verificam recursos, têm a maior fidelidade, geralmente são instrumentados e podem levar mais tempo para serem executados.
  • A arquitetura do app influencia a dificuldade dos testes.
  • O TDD ou desenvolvimento orientado a testes é uma estratégia em que você escreve os testes primeiro e depois cria o recurso para passar nos testes.
  • Para isolar partes do app para teste, use duplas de teste. Um teste duplo é uma versão de uma classe criada especificamente para testes. Por exemplo, você simula a obtenção de dados de um banco de dados ou da Internet.
  • Use a injeção de dependência para substituir uma classe real por uma de teste, por exemplo, um repositório ou uma camada de rede.
  • Use testes de instrumentação (androidTest) para iniciar componentes de interface.
  • Quando não é possível usar a injeção de dependência do construtor, por exemplo, para iniciar um fragmento, geralmente é possível 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", cujo objetivo é fornecer dependências para o código regular e de teste.

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.