Este tutorial faz parte do curso Android avançado em Kotlin. Você obterá o máximo valor deste curso se trabalhar com os tutoriais em sequência, mas isso não é obrigatório. Todos os tutoriais do curso estão listados na página de destino Android avançado em Kotlin.

Introdução

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

O que você já deveria saber

Você deve estar familiarizado com:

O que você aprenderá

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

O que você vai fazer

Nesta série de tutoriais, você trabalhará com o aplicativo TO-DO Notes. O aplicativo permite que você anote as tarefas a serem concluídas e as exiba em uma lista. Você pode então marcá-los como concluídos ou não, filtrá-los ou excluí-los.

Este aplicativo foi escrito em Kotlin, tem algumas telas, usa componentes Jetpack e segue a arquitetura de um Guia para arquitetura de aplicativo. Ao aprender como testar este aplicativo, você poderá testar aplicativos que usam as mesmas bibliotecas e arquitetura.

Abaixe o código

Para começar, baixe o código:

Baixe o Zip

Como alternativa, você pode clonar o repositório Github para o código:

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

Você pode navegar pelo código no android-testing Github repository.

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

Etapa 1: execute o aplicativo de amostra

Depois de baixar o aplicativo TO-DO, abra-o no Android Studio e execute-o. Deve compilar. Explore o aplicativo fazendo o seguinte:

Etapa 2: explore o código do aplicativo de amostra

O aplicativo TO-DO é baseado no popular exemplo de teste e arquitetura de Projetos de arquitetura (usando a versão de arquitetura reativa do exemplo). O aplicativo segue a arquitetura de um Guia para arquitetura de aplicativo. Ele usa ViewModels com Fragments, um repositório e Room. Se você estiver familiarizado com algum dos exemplos abaixo, este aplicativo tem uma arquitetura semelhante:

É mais importante que você entenda a arquitetura geral do aplicativo do que um entendimento profundo da lógica de qualquer camada.

Aqui está o resumo dos pacotes que você encontrará:

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

.addedittask

The add or edit a task screen: código da camada de IU para adicionar ou editar uma tarefa.

.data

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

.statistics

A tela de estatística: Código da camada de IU para a tela de estatísticas.

.taskdetail

A tela de detalhe de tarefa: código da camada de IU para uma única tarefa.

.tasks

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

.util

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

Camada de dados (.data)

Este aplicativo 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 e é o que retorna dados para a camada de IU.

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

Cada um dos pacotes da camada de IU contém um fragmento e um modelo de vista, junto com quaisquer outras classes que são necessárias para a IU (como um adaptador para a lista de tarefas). A TaskActivity é a atividade que contém todos os fragmentos.

Navegação

A navegação para o aplicativo é controlada pelo componente de navegação. Ele é definido no arquivo nav_graph.xml. A navegação é acionada nos modelos de vista usando a classe Event; os modelos de vista também determinam quais argumentos passar. Os fragmentos observam os Event se fazem a navegação real entre as telas.

Neste tutorial, você aprenderá como testar repositórios, modelos de vistas e fragmentos usando dublês de teste e injeção de dependência. Antes de mergulhar no que são, é importante entender o raciocínio que guiará o que e como você escreverá esses testes.

Esta seção cobre algumas das melhores práticas de teste em geral, conforme se aplicam ao Android.

A Pirâmide de Teste

Ao pensar sobre uma estratégia de teste, existem três aspectos de teste relacionados:

Existem trade-offs inerentes entre esses aspectos. Por exemplo, velocidade e fidelidade são uma compensação - quanto mais rápido o teste, geralmente, menos fidelidade e vice-versa. Uma maneira comum de dividir os testes automatizados é nestas três categorias:

A proporção sugerida desses testes é frequentemente representada por uma pirâmide, com a grande maioria dos testes sendo testes unitários.

Arquitetura e Teste

Sua capacidade de testar seu aplicativo em todos os níveis diferentes da pirâmide de teste está inerentemente ligada à app's architecture. Por exemplo, um aplicativo extremamente mal arquitetado pode colocar toda a sua lógica dentro de um método. Você pode ser capaz de escrever um teste de ponta a ponta para isso, já que esses testes tendem a testar grandes partes do aplicativo, mas que tal escrever testes unitários ou integração? Com todo o código em um somente 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 peça seja testada isoladamente. Arquitetura é uma forma de dividir e organizar seu código, o que permite um teste unitário e integração mais fácil. O aplicativo TO-DO que você testará segue uma arquitetura particular:



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

  1. Primeiro você fazer teste unitário o repositório.
  2. Em seguida, você usará um teste dublê no modelo de vista, que é necessário para teste unitário e teste de integração no modelo de vista.
  3. A seguir, você aprenderá a escrever testes de integração para fragmentos e seus modelos de vista.
  4. Finalmente, você aprenderá a escrever testes de integração que incluem o Navigation component.

O teste de ponta a ponta será abordado na próxima lição.

Quando você escreve um teste unitário para uma parte de uma classe (um método ou uma pequena coleção de métodos), seu objetivo é only test the code in that class.

Testar apenas o código em uma classe ou classes específicas pode ser complicado. Vejamos um exemplo. Abra a classe data.source.DefaultTaskRepository no conjunto de origem main. Este é o repositório do aplicativo e é a classe para a qual você escreverá os testes unitários a seguir.

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

Portanto, cada método em DefaultTaskRepository chama 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, dê uma olhada neste 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 para o seu repositório. Este método inclui a leitura de 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.

Aqui estão algumas razões mais específicas pelas quais testar o repositório é difícil:

Dublês de teste

A solução para isso é que quando você estiver testando o repositório, não use a rede real ou o código do banco de dados, mas use um dublê de teste. Um dublê de teste é uma versão de uma classe criada especificamente para teste. Destina-se a substituir a versão real de uma classe em testes. É semelhante a como um dublê é um ator especializado em dublês e substitui o ator real por ações perigosas.

Aqui estão alguns tipos de testes dublês:

Falso

Um teste dublê 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 dublê de teste que rastreia quais métodos foram chamados. Em seguida, ele passa ou falha em um teste, dependendo se seus métodos foram chamados corretamente.

Rascunho

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

Dummy???

Um dublê de teste que é passado, mas não usado, como se você somente precisasse fornecê-lo como um parâmetro. Se você tivesse um NoOpTaskRepository, ele apenas implementaria o TaskRepository com no código em qualquer um dos métodos.

Espião

Um dublê de teste que também rastreia algumas informações adicionais; por exemplo, se você fez um SpyTaskRepository, ele pode manter o controle do número de vezes que o método addTask foi chamado.

Para obter mais informações sobre dublês de teste, consulte ???Testando no banheiro: Conheça suas dublês de teste.

Os dublês de teste mais comuns usados ​​no Android são Falsos e Mocks.

Nesta tarefa, você criará um FakeDataSource dublê de teste para o teste unitário DefaultTasksRepository desacoplado das fontes de dados reais.

Etapa 1: crie a classe FakeDataSource

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

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

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

Etapa 2: implementar a interface TasksDataSource

Para poder usar sua nova classe FakeDataSource como um dublê de teste, ela deve ser capaz de 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 FakeDataSource implementar 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 Implement members.


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

Etapa 3: implemente o método getTasks em FakeDataSource

FakeDataSource é um tipo específico de teste dublê chamado de falso. Um falso é um teste dublê que tem uma implementação "funcional" da classe, mas é implementado de uma forma que o torna bom para testes, mas inadequado para produção. A implementação "funcional" significa que a classe produzirá resultados realistas com dados de entrada.

Por exemplo, sua fonte de dados falsa não se conectará à rede nem salvará nada em um banco de dados - em vez disso, ela apenas usará uma lista na memória. Isso "funcionará conforme o esperado", pois os métodos para obter ou salvar tarefas retornarão os resultados esperados, mas você nunca poderá usar essa implementação na produção, porque ela não é salva no servidor ou em um banco de dados.

Um FakeDataSource

  1. Altere 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 "fingem" ser um banco de dados ou resposta do servidor. Por enquanto, o objetivo é testar o método do repository'sgetTasks. Isso chama os métodos data source'sgetTasks, deleteAllTasks e saveTask.

Escreva uma versão falsa desses métodos:

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

Esses métodos, implementados para FakeDataSource, se parecem 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)
}

Aqui estão 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 a como as fontes de dados locais e remotas reais funcionam.

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

O principal problema é que você tem uma FakeDataSource, mas não está claro como você a usa nos testes. Ele precisa substituir o TasksRemoteDataSource e o TasksLocalDataSource, mas apenas nos testes. Ambos TasksRemoteDataSource e TasksLocalDataSource são dependências de DefaultTasksRepository, o que significa que DefaultTasksRepositories requer ou "depende" dessas classes para corre.

No momento, as dependências são construídas 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. Não há como trocar seu dublê de teste.

Em vez disso, o que você deseja fazer é provide essas fontes de dados para a classe, em vez de codificá-las permanentemente. Fornecer dependências é conhecido como dependency injection. Existem diferentes maneiras de fornecer dependências e, portanto, diferentes tipos de injeção de dependência.

Injeção de dependência de construtor permite que você troque o duplo de teste passando-o para o construtor.

Sem injeção

Injeção

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

  1. Altere o construtor do DefaultTaskRepository de receber??? um Application para receber as fontes de dados e o despachante de corrotina.

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ê passou as dependências, remova o método init. Você não precisa mais criar as dependências.
  2. Exclua também as variáveis ​​de instância antigas. Você está definindo-os no construtor:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Finalmente, 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 injeção de dependência de construtor!

Etapa 2: use seu FakeDataSource em seus testes

Agora que seu código está usando injeção de dependência de construtor, você pode usar sua fonte de dados falsa para testar seu DefaultTasksRepository.

  1. Clique com o botão direito no nome da classe DefaultTasksRepository e selecione Generate e, em seguida, Test.
  2. Siga os prompts para criar DefaultTasksRepositoryTest no conjunto de origem test.
  3. No topo de sua nova classe DefaultTasksRepositoryTest, adicione as variáveis ​​de membro abaixo para representar os dados em suas 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 de seu repositório) e uma variável para o DefaultTasksRepository que você testará.

DefaultTasksRepositoryTest.kt

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

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Faça um método para configurar e inicializar um DefaultTasksRepository testável. Este DefaultTasksRepository usará seu dublê de teste, FakeDataSource.

  1. Crie um método chamado createRepository e anote-o com @Before.
  2. Instancie suas fontes de dados falsas, usando as listas remoteTasks e localTasks.
  3. Instancie seu tasksRepository, usando as duas fontes de dados falsas que você acabou de criar e Dispatchers.Unconfined.

O método final deve ser semelhante ao código abaixo.

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: Escreva o teste DefaultTasksRepository getTasks()

É hora de escrever um teste DefaultTasksRepository!

  1. Escreva um teste para o método getTasks do repositório. Verifique se quando você chama getTasks com true (significando que ele deve recarregar da fonte de dados remota), ele retorna dados da fonte de dados remota (em oposição aos dados locais fonte).

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ê receberá um erro ao chamargetTasks:

Etapa 4: adicione 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 corotina. Para resolver esse erro, você precisará adicionar algumas dependências gradle para tratar com o lançamento de corrotinas em seus testes.

  1. Adicione as dependências necessárias para testar corrotinas ao conjunto de fonte 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, especificamente voltada para testar corrotinas. Para executar seus testes, use a função runBlockingTest. Esta é uma função fornecida pela biblioteca de teste de corrotinas. Ele pega um bloco de código e, em seguida, executa esse bloco de código em um contexto de corotina especial que é executado de forma síncrona e imediata, o que significa que as ações ocorrerão em uma ordem determinística. Isso essencialmente faz com que suas corrotinas sejam executadas como não corrotinas, portanto, ele serve para testar código.

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

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

Este teste final se parece 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 seu novo teste getTasks_requestsAllTasksFromRemoteDataSource e verifique se ele funciona e se o erro desapareceu!

Você acabou de ver como testar a unidade de um repositório. Nas próximas etapas, você usará novamente a injeção de dependência e criará outro dublê de teste - desta vez para mostrar como escrever testes unitários e integração para seus modelos de vista.

Os testes unitários devem apenas testar a classe ou método em que você está interessado. Isso é conhecido como teste em isolamento, em que você isola claramente sua "unidade" e testa apenas o código isso faz parte dessa unidade.

Portanto, TasksViewModelTest deve testar apenas o código TasksViewModel - não deve testar no banco de dados, rede ou classes de repositório. Portanto, para seus modelos de vista, da mesma forma que você acabou de fazer para seu repositório, você criará um repositório falso e aplicará injeção de dependência para usá-lo em seus testes.

Nesta tarefa, você aplica injeção de dependência para modelo de vistas.

Etapa 1. Crie uma interface TasksRepository

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

Como isso fica na prática? Observe TasksRemoteDataSource, TasksLocalDataSource e FakeDataSource e observe que todos compartilham a mesma interface: TasksDataSource. Isso permite que você diga no construtor de DefaultTasksRepository que você obtém um TasksDataSource.

DefaultTasksRepository.kt

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

Isso é o que nos permite trocar seu FakeDataSource!

A seguir, faça uma interface para DefaultTasksRepository, como você fez para as fontes de dados. Ele precisa incluir todos os métodos públicos (superfície de API pública) de DefaultTasksRepository.

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

  1. Escolha Extract to separate file.

  1. Na janela Extract Interface, altere o nome da interface para TasksRepository.
  2. Na seção Members to form interface, verifique todos os membros except os dois membros complementares e os métodos private.


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

E DefaultTasksRepository agora implementa TasksRepository.

  1. Execute seu aplicativo (não os testes) para se certificar de que tudo ainda está funcionando.

Etapa 2. Crie FakeTestRepository

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

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

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Você será informado de que precisa implementar os métodos de interface.

  1. Passe o mouse sobre o erro até ver o menu de sugestões e, em seguida, clique e selecione Implement members.
  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". Semelhante a 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 FakeDataSource s ou algo parecido; ele somente precisa retornar saídas falsas realistas dadas as entradas. Você usará um LinkedHashMap para armazenar a lista de tarefas e um MutableLiveData para suas tarefas observáveis.

  1. Em FakeTestRepository, adicione uma variável LinkedHashMap representando a lista atual de tarefas e um MutableLiveData para suas 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—Este método deve pegar o tasksServiceData e transformá-lo em uma lista usando tasksServiceData.values.toList() e então retornar isso como um Success.
  2. refreshTasks—Atualiza o valor de observableTasks para ser o que é retornado por getTasks().
  3. observeTasks- Cria uma corrotina usando runBlocking e executa refreshTasks e retorna observableTasks.

Abaixo está o código para esses 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 de teste para adicionar tarefas

Ao testar, é melhor ter algumas Tasks já em seu repositório. Você poderia chamar saveTask várias vezes, mas para tornar isso mais fácil, adicione um método auxiliar especificamente para testes que permite adicionar tarefas.

  1. Adicione o método addTasks, que leva em uma vararg de tarefas, adiciona cada uma ao HashMap e, em seguida, 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 teste com alguns dos principais métodos implementados. Em seguida, use isso em seus testes!

Nesta tarefa, você usa uma classe falsa dentro de um ViewModel. Use injeção de dependência de construtor, para obter as duas fontes de dados por meio de injeção de dependência de construtor, adicionando uma variável TasksRepository ao construtor de TasksViewModel.

Este processo é um pouco diferente com os modelos de vista porque você não os constrói diretamente. Por exemplo:

class TasksFragment : Fragment() {

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

}


Como no código acima, você está usando o viewModel's delegado de propriedade que cria o modelo de vista. Para alterar como o modelo de vista é construído, você precisará adicionar e usar um ViewModelProvider.Factory. Se você não está familiarizado com ViewModelProvider.Factory, pode aprender mais sobre ele aqui.

Etapa 1. Faça e use um ViewModelFactory em TasksViewModel

Você começa atualizando as classes e testes relacionados à tela Tasks.

  1. Abra TasksViewModel.
  2. Altere o construtor de TasksViewModel para incluir 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 TasksViewModel, mas você também pode colocá-la em seu próprio arquivo.

  1. Na parte inferior do arquivo TasksViewModel, fora da classe, adicione um TasksViewModelFactory que contém 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 alterar a forma como os ViewModel s são construídos. Agora que você tem a fábrica, use-a sempre que construir seu modelo de vista.

  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 seu app e certifique-se de que tudo ainda está funcionando!

Etapa 2. Use FakeTestRepository dentro de TasksViewModelTest

Agora, em vez de usar o repositório real em seus testes de modelo de vista, você pode usar o repositório falso.

  1. Abra TasksViewModelTest.
  2. Adicione uma propriedade FakeTestRepository em 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 fazer um FakeTestRepository com três tarefas e, em seguida, construa o tasksViewModel com este 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 pode remover o código @RunWith(AndroidJUnit4::class) anotação.
  2. Execute seus testes, certifique-se de que todos ainda funcionam!

Usando injeção de dependência de construtor, você removeu o DefaultTasksRepository como uma dependência e o substituiu por seu FakeTestRepository nos testes.

Etapa 3. Atualize também o fragmento TaskDetail e o ViewModel

Faça exatamente as mesmas alterações para TaskDetailFragment e TaskDetailViewModel. Isso irá preparar o código para quando você escrever os testes TaskDetail a seguir.

  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 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 seu código e verifique se tudo está funcionando.

Agora você pode usar um FakeTestRepository em vez do repositório real em TasksFragment e TasksDetailFragment.

Em seguida, você escreverá testes de integração para testar suas interações de fragmento e modelo de vista. Você descobrirá se o código do seu modelo de vista atualiza apropriadamente sua IU. Para fazer isso você usa

Testes de integração testam a interação de várias classes para certificar-se de que 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ê pegará cada fragmento e escreverá testes de integração para o fragmento e o modelo de vista 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:

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

Etapa 2. Faça uma classe TaskDetailFragmentTest

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

Você começará escrevendo um teste de fragmento para o TaskDetailFragment, uma vez que possui uma funcionalidade bastante básica em comparação com os outros fragmentos.

  1. Abra taskdetail.TaskDetailFragment.
  2. Generate um teste para TaskDetailFragment, como você fez antes. Aceite as opções padrão e coloque-o 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 é:

Etapa 3. Lançar um fragmento de um teste

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

  1. Copie este teste em 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:

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

  1. Este é um teste instrumentado, então certifique-se de que the emulator or your device is visible.
  2. Execute o teste.

Algumas coisas devem acontecer.

Por fim, observe atentamente e observe que o fragmento diz "Sem dados", pois não carrega com êxito os dados da tarefa.

Seu teste precisa carregar o TaskDetailFragment (o que você fez) e confirmar que os dados foram carregados corretamente. Por que não há dados? Isso ocorre 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 este FakeTestRepository, mas precisa de alguma maneira de substituir seu repositório real por um falso para seu fragmento. Você fará isso a seguir!

Nesta tarefa, você fornecerá seu repositório falso para seu fragmento usando um ServiceLocator. Isso permitirá que você escreva seus testes de integração para fragmentos e modelo de vistas.

Você não pode usar injeção de dependência de construtor aqui, como fez antes, quando precisava fornecer uma dependência para o modelo de vista ou repositório. A injeção de dependência do construtor requer 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.

Visto que você não constrói o fragmento, você não pode usar injeção de dependência de construtor para trocar o dublê de teste do repositório (FakeTestRepository) para o fragmento. Em vez disso, use o padrão Service Locator. O padrão Service Locator é uma alternativa para a injeção de dependência. Envolve a criação de uma classe singleton chamada "Service Locator", cujo objetivo é fornecer dependências, tanto para o código regular quanto para o de teste. No código do aplicativo regular (o conjunto de origem main), todas essas dependências são as dependências regulares do aplicativo. Para os testes, você modifica o Service Locator para fornecer versões dublês de teste das dependências.

Não usando o Service Locator


Usando um localizador de serviço

Para este aplicativo tutorial, faça o seguinte:

  1. Crie uma classe Service Locator que seja capaz de construir e armazenar um repositório. Por padrão, ele constrói um repositório "normal".
  2. Refatore seu código para que, quando precisar de um repositório, use o Service Locator.
  3. Em sua classe de teste, chame um método no Service Locator que troque o repositório "normal" com seu dublê de teste.

Etapa 1. Crie o ServiceLocator

Vamos fazer uma classe ServiceLocator. Ele residirá no conjunto de código-fonte principal com o resto do código do aplicativo, porque é usado pelo código do aplicativo principal.

Nota: O ServiceLocator é um singleton, então use a Kotlin object palavra-chave 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 de database e repository e defina ambas como null.
  4. Anote o repositório com @Volatile porque pode ser usado por vários threads (@Volatile é explicado em detalhes aqui)

Seu código deve ser semelhante ao mostrado abaixo.

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 retornará um DefaultTasksRepository pré-existente 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. Este método deve ser synchronized this para evitar, em situações com múltiplas threads em execução, criar acidentalmente duas instâncias do repositório.
  2. createTasksRepository—Código para criar um novo repositório. 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.

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

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

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

  1. No nível superior de sua hierarquia de pacote, abra TodoApplication e crie um val para seu repositório e atribua a ele um repositório que é 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, pode remover o antigo método getRepository 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 todos os lugares que você estiver usando getRepository, use o taskRepository do aplicativo. Isso garante que em vez de criar o repositório diretamente, você receberá qualquer repositório que o ServiceLocator forneceu.

  1. Abra TaskDetailFragement e encontre a chamada para getRepository no início da classe.
  2. Substitua esta chamada por uma chamada que obtém 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 de TodoApplication.

TasksFragment.kt

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



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

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

Como você apenas refatorou, o aplicativo deve ser executado da mesma forma sem problemas.

Etapa 3. Crie FakeAndroidTestRepository

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

  1. Clique com botão da direita no conjunto de origem androidTest e crie um pacote de data. Clique com o botão direito novamente e crie um pacote source.
  2. Faça uma nova classe neste pacote de origem chamada FakeAndroidTestRepository.kt.
  3. Copie o seguinte código 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. Prepare seu ServiceLocator para testes

Ok, é hora de usar o ServiceLocator para trocar dublês de teste durante o teste. Para fazer isso, você precisa adicionar algum código ao seu código ServiceLocator.

  1. Abra ServiceLocator.kt.
  2. Marque o ???setter para tasksRepository como @VisibleForTesting. Essa anotação é uma forma de expressar que o motivo do setter ser público é por causa dos testes.

ServiceLocator.kt

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

Quer você execute seu teste sozinho ou em um grupo de testes, seus testes devem ser executados exatamente da mesma forma. O que isso significa é que seus testes não devem ter comportamento dependente um do outro (o que significa evitar o compartilhamento de objetos entre os testes).

Como o ServiceLocator é um singleton???, ele tem a possibilidade de ser acidentalmente compartilhado 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

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. Use seu 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 um FakeAndroidTestRepository antes de cada teste e limpe-o 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. Envolva o corpo da função de activeTaskDetails_DisplayedInUi() em runBlockingTest.
  2. Salve activeTask no repositório antes de lançar o fragmento.
repository.saveTask(activeTask)

O teste final se parece com o 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. Anote toda a classe com @ExperimentalCoroutinesApi.

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

Muito parecido com antes, você deve ver o fragmento, exceto que desta vez, porque você configurou corretamente o repositório, ele agora mostra 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ê estruturou seu código para que possa adicionar testes com asserções para sua IU. Para fazer isso, você usará a Espresso testing library.

O Espresso ajuda você a:

Etapa 1. Observe a dependência do Gradle

Você já terá a dependência principal do Espresso, pois ela está 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- Esta dependência principal do Espresso é incluída por padrão quando você faz um novo projeto Android. Ele contém o código de teste básico para a maioria das vistas e ações neles.

Etapa 2. Desligue as 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 atrasar e você tentar testar se uma vista está na tela, mas ainda está animando, o Espresso pode falhar acidentalmente em um teste. Isso pode tornar os testes do Espresso fragmentados.

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

  1. Em seu dispositivo de teste, vá para Settings > Developer options.
  2. Desative estas três configurações: Window animation scale, Transition animation scale e Animator duration scale.

Etapa 3. Observe um teste do Espresso

Antes de escrever um teste do Espresso, dê uma olhada em alguns códigos do Espresso.

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

O que esta instrução faz é encontrar a vista da caixa de seleção com a id task_detail_complete_checkbox, clicar nela e, em seguida, afirmar que ela está marcada.

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

1. Static Espresso method

onView

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

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId é um exemplo de ViewMatcher que obtém uma vista por seu ID. Existem outros matchers de vista que você pode consultar na documentação.

3. ViewAction

perform(click())

O método perform que leva uma ViewAction. Uma ViewAction é algo que pode ser feito na vista, por exemplo aqui, é clicar na vista.

4. ViewAssertion

check(matches(isChecked()))

check que leva uma ViewAssertion. ViewAssertion s verificam ou afirmam algo sobre a vista. A ViewAssertion mais comum que você usará é a asserção matches. Para finalizar a asserção, use outro ViewMatcher, neste caso isChecked.

Observe que você nem sempre chama perform e check em uma instrução Espresso. Você pode ter instruções que apenas fazem uma afirmação usando check ou apenas fazem uma 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())))
    }

Aqui estão 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 após o comentário // THEN usa Espresso. Examine a estrutura de teste e o uso de withId e verifique para fazer afirmações sobre como a página de detalhes deve ser.
  2. Execute o teste e confirme se ele foi aprovado.

Etapa 4. Opcional, escreva seu próprio teste Espresso

Agora escreva um teste você mesmo.

  1. Crie um novo teste chamado completedTaskDetails_DisplayedInUi e copie este código de 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. Olhando para o teste anterior, complete este teste.
  2. Execute e confirme se o teste foi aprovado.

O completedTaskDetails_DisplayedInUi finalizado deve se parecer 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ê aprenderá como testar o componente de navegação, usando um tipo diferente de teste dublê chamado mock, e a biblioteca de teste Mockito.

Neste tutorial, você usou um teste dublê chamado falso. Falsos são um dos muitos tipos de dublês de teste. Qual teste dublê 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 para uma tela de detalhes da tarefa.

Este é o código em TasksFragment que navega para uma tela de detalhes da tarefa 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 ao método navigate. Se você precisar escrever uma declaração assert, não há uma maneira direta de testar se você navegou até TaskDetailFragment. Navegar é 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. Isso é exatamente o que um dublê de teste mock faz - ele verifica se métodos específicos foram chamados.

Mockito é uma estrutura para fazer dublês de teste. Embora a palavra mock seja usada na API e no nome, ela não serve apenas para fazer simulações. Também pode fazer rascunhos e espiões.

Você usará o Mockito para fazer um NavigationController mock que pode afirmar que o método de navegação foi chamado corretamente.

Etapa 1. Adicionar dependências do Gradle

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



Etapa 2. Criar TasksFragmentTest

  1. Abra TasksFragment.
  2. Clique com o botão direito no nome da classe TasksFragment e selecione Generate e depois Test. Crie um teste no conjunto de origem androidTest.
  3. Copie este código para 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()
    }

}

Este código é semelhante ao código TaskDetailFragmentTest que você escreveu. Ele configura e destrói um FakeAndroidTestRepository. Adicione um teste de navegação para testar se, ao clicar em uma tarefa na lista de tarefas, você será direcionado ao 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 mock.

TasksFragmentTest.kt

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

Para zombar??? no Mockito, passe na aula que deseja zombar.

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

  1. Faça seu novo mock do NavController do fragmento.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Adicione o código para clicar no item do RecyclerView que contém 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()))

RecyclerViewActions faz parte da biblioteca espresso-contrib e permite que você execute 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 torna isso uma simulação - você pode confirmar o navController simulado chamado de método específico (navigate) com um parâmetro (actionTasksFragmentToTaskDetailFragment com o ID de "id1").

O teste completo é 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 seu teste!

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

  1. Use o Mockito para criar um mock do NavController.
  2. Anexe aquele NavController simulado ao fragmento.
  3. Verifique se a navegação foi chamada com a ação e os parâmetros corretos.

Etapa 3. Opcional, escreva clickAddTaskButton_navigateToAddEditFragment

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

  1. Escreva o teste clickAddTaskButton_navigateToAddEditFragment que verifica se você clicar em + FAB, você navegará para 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 que você iniciou e o código final.

Para baixar o código do tutorial concluído, você pode usar o comando git abaixo:

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


Alternativamente, você pode baixar o repositório como um arquivo Zip, descompactá-lo e abri-lo no Android Studio.

Baixe o Zip

Este tutorial abordou como configurar injeção de dependência manual, um localizador de serviço e como usar falsos e simulações??? em seus aplicativos Android Kotlin. Em particular:

Amostras:

Documentação do desenvolvedor Android:

Vídeos:

De outros:

Para obter links para outros tutoriais neste curso, consulte a página inicial de tutoriais do Android avançado em Kotlin.