Este codelab faz parte do curso Android avançado no Kotlin. Você aproveitará mais o curso se fizer os codelabs em sequência, mas isso não é obrigatório. Todos os codelabs do curso estão listados na página de destino dos codelabs avançados do Android em Kotlin (link em inglês).
Introdução
Este segundo codelab de teste trata sobre testes duplos: quando usá-los no Android e como implementá-los usando a injeção de dependência, o padrão Localizador de serviços e bibliotecas. Ao fazer isso, você aprenderá a escrever:
- Testes de unidade de repositório
- Fragmentos e testes de integração de viewmodel
- Testes de navegação de fragmentos
O que você já precisa saber
Você precisa:
- A linguagem de programação Kotlin
- Conceitos de teste abordados no primeiro codelab: como criar e executar testes de unidade no Android usando o JUnit, Hamcrest, teste AndroidX, Robolectric e teste de LiveData
- As seguintes bibliotecas principais do Android Jetpack:
ViewModel
,LiveData
e componente de navegação - Arquitetura de aplicativos, seguindo o padrão do Guia para a arquitetura do app e dos codelabs do curso Conceitos básicos do Android
- Noções básicas de corrotinas no Android
O que você vai aprender
- Como planejar uma estratégia de testes
- Como criar e usar testes duplos, ou seja, simulações e simulações
- Como usar a injeção de dependência manual no Android para testes de unidade e integração.
- Como aplicar o padrão do localizador de serviços
- Como testar repositórios, fragmentos, modelos de visualização e o componente de navegação.
Você usará as seguintes bibliotecas e conceitos de código:
runBlocking
erunBlockingTest
FragmentScenario
- Espresso
- Mockito (em inglês)
Atividades do laboratório
- gravar testes de unidade para um repositório usando um teste duplo e injeção de dependência;
- Criar testes de unidade para um modelo de visualização usando um teste duplo e injeção de dependência.
- Programar testes de integração para fragmentos e modelos de visualização usando o framework de testes de IU do Espresso.
- Crie testes de navegação usando o Mockito e o Espresso.
Nesta série de codelabs, você trabalhará com o app TO-DO Notes. Com ele, você pode anotar as tarefas a serem concluídas e exibi-las em uma lista. Você pode marcá-los como concluídos ou não, filtrá-los ou excluí-los.
Este app foi escrito em Kotlin, tem algumas telas, usa componentes do Jetpack e segue a arquitetura de um Guia para a arquitetura do app. Ao aprender a testar esse app, você poderá testar apps que usam as mesmas bibliotecas e arquiteturas.
Fazer o download do código
Para começar, faça o download do código:
Como alternativa, é possível clonar o repositório do GitHub para o código:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
Reserve um momento para se familiarizar com o código, seguindo as instruções abaixo.
Etapa 1: executar o app de exemplo
Depois de fazer o download do app de tarefas, abra-o no Android Studio e execute-o. Ele será compilado. Explore o aplicativo fazendo o seguinte:
- Crie uma nova tarefa com o botão de ação flutuante flutuante. Digite um título primeiro e, em seguida, informações adicionais sobre a tarefa. Salve-a com o FAB de verificação verde.
- Na lista de tarefas, clique no título da tarefa que você acabou de concluir e observe a tela de detalhes dela para ver o restante da descrição.
- Na lista ou na tela de detalhes, marque a caixa de seleção da tarefa para definir o status como Concluída.
- Volte para a tela "Tarefas", abra o menu e filtre as tarefas pelo status Ativo e Concluído.
- Abra a gaveta de navegação e clique em Estatísticas.
- Volte para a tela "Visão geral". No menu da gaveta de navegação, selecione Limpar concluídas para excluir todas as tarefas com o status Concluída.
Etapa 2: analisar o exemplo de código do app
O app de tarefas é baseado no conhecido exemplo de arquitetura e teste Architecture Blueprints, que usa a versão de arquitetura reativa do exemplo. O app segue a arquitetura de um Guia para a arquitetura do app. Ele usa ViewModels com fragmentos, um repositório e Room. Se você conhece algum dos exemplos abaixo, esse app tem uma arquitetura semelhante:
- Codelab Room com uma visualização
- Codelabs do treinamento do curso Conceitos básicos do Kotlin para Android
- Codelabs de treinamento avançado do Android
- Exemplo do Android Sunflower (link em inglês)
- Curso de treinamento sobre desenvolvimento de apps Android com a Udacity em Kotlin (link em inglês)
É mais importante entender a arquitetura geral do app do que ter um profundo conhecimento da lógica em qualquer camada.
Veja o resumo dos pacotes que você encontrará:
Pacote: | |
| Adicionar ou editar uma tela de tarefas: código da camada de IU para adicionar ou editar uma tarefa. |
| 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. |
| Tela de estatísticas: código da camada de IU para a tela de estatísticas. |
| Tela de detalhes da tarefa:código da camada de IU para uma única tarefa. |
| Tela de tarefas: código da camada de IU para a lista de todas as tarefas. |
| Classes de utilitários:classes compartilhadas usadas em várias partes do app, por exemplo, para o layout de atualização de deslize usado em várias telas. |
Camada de dados (.data)
Esse app inclui uma camada de rede simulada no pacote remoto e uma camada de banco de dados no pacote local. Para simplificar, neste projeto a camada de rede é simulada com apenas um HashMap
com atraso, em vez de fazer solicitações de rede reais.
As coordenadas de DefaultTasksRepository
ou mediam entre a camada de rede e a de banco de dados. É isso que retorna os dados para a camada de IU.
Camada de IU ( .addedittask, .statistics, .taskdetail, .tasks)
Cada um dos pacotes de camada de IU contém um fragmento e um modelo de visualização, além de outras classes necessárias para a IU (como um adaptador para a lista de tarefas). O TaskActivity
é a atividade que contém todos os fragmentos.
Navegação
A navegação do app é controlada pelo componente de navegação. Ela é definida no arquivo nav_graph.xml
. A navegação é acionada nos modelos de visualização usando a classe Event
. Os modelos de visualização também determinam quais argumentos transmitir. Os fragmentos observam as Event
s e fazem a navegação real entre as telas.
Neste codelab, você aprenderá a testar repositórios, visualizar modelos e fragmentos usando testes duplos e injeção de dependência. Antes de nos aprofundarmos neles, é importante entender o que motivará você a programar os testes.
Esta seção aborda algumas práticas recomendadas dos testes em geral, já que elas se aplicam ao Android.
Pirâmide de teste
Ao pensar em uma estratégia de testes, há três aspectos relacionados:
- Escopo: quanto do código o teste toca? Os testes podem ser executados com um único método, em todo o aplicativo ou em algum lugar entre eles.
- Velocidade: qual é a velocidade do teste? As velocidades de teste podem variar de milissegundos a vários minutos.
- Fidelidade: até que ponto é o teste real? Por exemplo, se parte do código que você está testando precisar fazer uma solicitação de rede, o código do teste faz essa solicitação de rede ou é um resultado falso? Se o teste realmente se comunicar com a rede, significa que ele tem maior fidelidade. A desvantagem é que o teste pode levar mais tempo para ser executado, pode resultar em erros caso a rede esteja inativa ou possa ser caro.
Há vantagens e desvantagens inerentes entre esses aspectos. Por exemplo, velocidade e fidelidade são compensadas. Quanto mais rápido o teste, geralmente, menos fidelidade e vice-versa. Uma maneira comum de dividir testes automatizados é nestas três categorias:
- Testes de unidade: são testes altamente focados em uma única classe, geralmente um único método nessa classe. Se um teste de unidade falhar, você saberá exatamente onde o problema está no código. A fidelidade deles é baixa porque, no mundo real, seu app envolve muito mais do que a execução de um método ou uma classe. Eles são rápidos o suficiente para serem executados sempre que você altera o código. Na maioria das vezes, eles serão executados localmente no conjunto de origem
test
. Exemplo: como testar métodos únicos em modelos de visualização e repositórios. - Testes de integração: testam a interação de várias classes para garantir que elas se comportem conforme esperado quando usadas juntas. Uma maneira de estruturar testes de integração é fazer com que eles testem um único recurso, como a capacidade de salvar uma tarefa. Eles testam um escopo de código maior que os testes de unidade, mas ainda são otimizados para funcionar rapidamente em comparação com a fidelidade total. Eles podem ser executados localmente ou como testes de instrumentação, dependendo da situação. Exemplo: como testar toda a funcionalidade de um único fragmento e visualizar o par de modelos.
- Testes completos (E2e): teste uma combinação de recursos funcionando em conjunto. Eles testam grandes partes do app, simulam o uso real de perto e, portanto, geralmente são lentos. Eles têm a maior fidelidade e informam que seu aplicativo realmente funciona como um todo. De modo geral, esses testes serão instrumentados (no conjunto de origem de
androidTest
).
Exemplo: inicializar todo o app e testar alguns recursos juntos.
A proporção sugerida desses testes geralmente é representada por uma pirâmide, com a grande maioria dos testes sendo testes de unidade.
Arquitetura e teste
Sua capacidade de testar seu aplicativo em todos os diferentes níveis da pirâmide de teste está inerentemente ligada à arquitetura do seu aplicativo. Por exemplo, um aplicativo extremamente com arquitetura incorreta pode colocar toda a lógica dentro de um método. Você pode programar um teste completo para isso, já que eles geralmente testam grande parte do app, mas e quanto a testes de unidade ou de integração? Com todo o código em um só lugar, é difícil testar apenas o código relacionado a uma única unidade ou recurso.
Uma abordagem melhor seria dividir a lógica do aplicativo em vários métodos e classes, permitindo que cada parte seja testada isoladamente. A arquitetura é uma maneira de dividir e organizar o código, o que facilita o teste de unidade e integração. O app de tarefas que você testará segue uma arquitetura específica:
Nesta lição, você verá como testar partes da arquitetura acima de forma adequada:
- Primeiro você fará o teste da unidade do repositório.
- Em seguida, você usará um teste duplo no modelo de visualização, que é necessário para o teste de unidade e o teste de integração do modelo de visualização.
- Em seguida, você aprenderá a criar testes de integração para fragmentos e modelos de visualização.
- Por fim, você vai aprender a criar testes de integração que incluem o componente de navegação.
O teste completo será abordado na próxima lição.
Quando você escreve um teste de unidade para uma parte de uma classe (um método ou um pequeno conjunto de métodos), a meta é testar somente o código dessa classe.
Testar apenas o código em classes específicas pode ser complicado. Veja um exemplo. Abra a classe data.source.DefaultTaskRepository
no conjunto de origem main
. Esse é o repositório do app. Essa é a classe em que você criará testes de unidade para o próximo caso.
Seu objetivo é testar somente o código dessa classe. Ainda assim, DefaultTaskRepository
depende de outras classes, como LocalTaskDataSource
e RemoteTaskDataSource
, para funcionar. Em outras palavras, LocalTaskDataSource
e RemoteTaskDataSource
são dependências de DefaultTaskRepository
.
Portanto, todos os métodos em DefaultTaskRepository
chamam métodos em classes de fonte de dados, que, por sua vez, chamam métodos em outras classes para salvar informações em um banco de dados ou se comunicar com a rede.
Por exemplo, veja este método em DefaultTasksRepo
.
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}
getTasks
é uma das chamadas mais "quot;basic" que você pode fazer para seu repositório. Esse método inclui ler um banco de dados SQLite e fazer chamadas de rede (a chamada para updateTasksFromRemoteDataSource
). Isso envolve muito mais código do que apenas o código do repositório.
Veja alguns motivos mais específicos para dificultar o teste do repositório:
- Você precisa pensar em como criar e gerenciar um banco de dados para realizar até os testes mais simples. Isso traz perguntas como "isso deve ser um teste local ou instrumentado?" e se você deveria usar o AndroidX Test para ter um ambiente Android simulado.
- Algumas partes do código, como o código de rede, podem demorar muito para serem executadas ou até mesmo apresentarem falhas ao criar testes lentos e de longa duração.
- Os testes podem perder a capacidade de diagnosticar qual código é responsável por uma falha. Os testes podem começar a testar códigos que não são de repositório, por isso, por exemplo, os supostos testes de unidade podem ocorrer devido a um problema em alguns dos códigos dependentes, como o código de banco de dados.
Duplas de teste
A solução para isso é que, ao testar o repositório, não use o código real do banco de dados ou da rede, mas, em vez disso, use um teste duplo. Um teste duplo é uma versão de uma classe criada especificamente para testes. Ele substitui a versão real de uma classe em testes. É semelhante à forma como um dublê é um ator especializado em acrobacias e substitui o ator real por ações perigosas.
Veja alguns tipos de dupla de testes:
Falso | Um double de teste que tenha uma implementação "funcionando” da classe, mas que seja implementado de forma que seja boa para testes, mas não adequado para produção. |
Simulação | Um teste duplo que rastreia quais métodos foram chamados. Em seguida, ele é aprovado ou reprovado em um teste, dependendo de se os métodos foram chamados corretamente. |
Stub (em inglês) | Um teste duplo que não inclui lógica e retorna apenas o que você programou para retornar. Um |
Dummy | Um teste duplo que é transmitido, mas não usado, como se você só precisa fornecê-lo como um parâmetro. Se você tivesse um |
Espionagem | Um teste duplo que também monitora algumas informações adicionais. Por exemplo, se você criou uma |
Para saber mais sobre duplas de teste, confira Testes no vaso sanitário: conheça suas duplas de teste.
As duplicações mais comuns usadas no Android são Fakes e Mocks.
Nesta tarefa, você criará um teste FakeDataSource
duplo para o teste de unidade DefaultTasksRepository
separado das fontes de dados reais.
Etapa 1: criar a classe FakeDataSource
Nesta etapa, você criará uma classe com o nome FakeDataSouce
, que será um teste duplo de LocalDataSource
e RemoteDataSource
.
- No conjunto de origem test, clique com o botão direito do mouse e selecione New -> Package.
- Crie um pacote data com um pacote source.
- Crie uma nova classe chamada
FakeDataSource
no pacote data/source.
Etapa 2: implementar a interface TasksDataSource
Para poder usar a nova classe FakeDataSource
como teste duplo, ela precisa substituir as outras fontes de dados. Essas fontes são TasksLocalDataSource
e TasksRemoteDataSource
.
- Observe como esses dois implementam a interface
TasksDataSource
.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
- Faça com que
FakeDataSource
implementeTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
O Android Studio reclamará que você não implementou os métodos necessários para TasksDataSource
.
- Use o menu de correção rápida e selecione Implementar membros.
- Selecione todos os métodos e pressione OK.
Etapa 3: implementar o método getTasks no FakeDataSource
FakeDataSource
é um tipo específico de teste duplo chamado de falso. Falso é um conjunto duplo de testes que tem uma implementação "funcionando” da classe, mas é implementado de uma forma que o torna adequado para testes, mas inadequado para produção. "Trabalho" significa que a classe produzirá saídas realistas de acordo com as entradas.
Por exemplo, a fonte de dados falsa não se conectará à rede nem salvará nada em um banco de dados. Em vez disso, ela usará apenas uma lista na memória. Isso funcionará como esperado nos métodos para receber ou salvar tarefas. No entanto, essa implementação nunca será usada, porque ela não é salva no servidor ou em um banco de dados.
Um FakeDataSource
- permite que você teste o código no
DefaultTasksRepository
sem precisar depender de um banco de dados ou de uma rede real. - oferece uma implementação "quão real" para os testes.
- Mude o construtor
FakeDataSource
para criar umvar
chamadotasks
, que é umMutableList<Task>?
com um valor padrão de uma lista mutável vazia.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
Esta é a lista de tarefas que "são" respostas a um banco de dados ou a um servidor. Por enquanto, a meta é testar o método getTasks
do repositório. Isso chama os métodos fonte de dados getTasks
, deleteAllTasks
e saveTask
.
Grave uma versão falsa destes métodos:
- Escreva
getTasks
: setasks
não fornull
, retorne um resultadoSuccess
. Setasks
fornull
, retorna um resultadoError
. - Gravar
deleteAllTasks
: limpe a lista de tarefas mutáveis. - Escrever
saveTask
: adicione a tarefa à lista.
Esses métodos, implementados para FakeDataSource
, são semelhantes ao código abaixo.
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}
Veja as instruções de importação, se necessário:
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
Isso é parecido com o funcionamento das fontes de dados locais e remotas.
Nesta etapa, você usará uma técnica chamada "injeção manual de dependência" para usar o dobro do teste falso que acabou de criar.
O principal problema é que você tem um FakeDataSource
, mas não está claro como ele é usado nos testes. Ele precisa substituir a TasksRemoteDataSource
e o TasksLocalDataSource
, mas apenas nos testes. Tanto TasksRemoteDataSource
quanto TasksLocalDataSource
são dependências de DefaultTasksRepository
, o que significa que DefaultTasksRepositories
requer ou "dependente" dessas classes para ser executado.
No momento, as dependências são criadas dentro do método init
do DefaultTasksRepository
.
DefaultTasksRepository.kt (link em inglês)
class DefaultTasksRepository private constructor(application: Application) {
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
// Some other code
init {
val database = Room.databaseBuilder(application.applicationContext,
ToDoDatabase::class.java, "Tasks.db")
.build()
tasksRemoteDataSource = TasksRemoteDataSource
tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
}
// Rest of class
}
Como você está criando e atribuindo taskLocalDataSource
e tasksRemoteDataSource
dentro de DefaultTasksRepository
, eles são essencialmente codificados. Não é possível trocar o teste duplo.
O que você quer fazer é fornecer essas origens de dados à classe em vez de codificá-las. O fornecimento de dependências é conhecido como injeção de dependência. Há maneiras diferentes de fornecer dependências e, portanto, diferentes tipos de injeção de dependência.
A Builder Dependency Injection permite alternar no teste duplo transmitindo-a para o construtor.
Sem injeção | Injeção |
Etapa 1: usar a injeção de dependência do construtor em DefaultTasksRepository
- Mude o construtor do
DefaultTaskRepository
para incluir umaApplication
nas duas origens de dados e no agente de corrotina. Você também precisará fazer isso para seus testes. Isso é descrito em mais detalhes na terceira seção da aula sobre corrotinas.
DefaultTasksRepository.kt (link em inglês)
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
- Como você transmitiu as dependências, remova o método
init
. Não é mais necessário criar as dependências. - Exclua também as antigas variáveis de instância. Você os define no construtor:
DefaultTasksRepository.kt (link em inglês)
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- Por fim, atualize o método
getRepository
para usar o novo construtor:
DefaultTasksRepository.kt (link em inglês)
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}
Agora você está usando a injeção de dependência do construtor.
Etapa 2: usar o FakeDataSource nos testes
Agora que o código está usando a injeção de dependência do construtor, você pode usar uma origem de dados falsa para testar o DefaultTasksRepository
.
- Clique com o botão direito do mouse no nome da classe
DefaultTasksRepository
, selecione Gerar e Testar. - Siga as instruções para criar um
DefaultTasksRepositoryTest
no conjunto de origem test. - Na parte superior da nova classe
DefaultTasksRepositoryTest
, adicione as variáveis de membro abaixo para representar as informações das suas fontes de dados falsas.
DefaultTasksRepositoryTest.kt (link em inglês)
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
- Crie três variáveis, duas variáveis de membro
FakeDataSource
(uma para cada fonte de dados do repositório) e uma variável paraDefaultTasksRepository
que será testada.
DefaultTasksRepositoryTest.kt (link em inglês)
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
Criar um método para configurar e inicializar um DefaultTasksRepository
testável. Este DefaultTasksRepository
usará o teste duplo, FakeDataSource
.
- Crie um método com o nome
createRepository
e adicione a anotação@Before
. - Instancie suas origens de dados falsas usando as listas
remoteTasks
elocalTasks
. - Instancie o
tasksRepository
usando as duas fontes de dados falsas que você acabou de criar e aDispatchers.Unconfined
.
O método final ficará assim:
DefaultTasksRepositoryTest.kt (link em inglês)
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
Etapa 3: gravar o elemento TaskTasksRepository getTasks()
É hora de criar um teste de DefaultTasksRepository
.
- Escreva um teste para o método
getTasks
do repositório. Verifique se, ao chamargetTasks
com otrue
(o que significa que ele precisa ser atualizado da fonte de dados remota), ele retorna dados da fonte de dados remota, não da fonte local.
DefaultTasksRepositoryTest.kt (link em inglês)
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
Você verá um erro ao ligar para getTasks:
Etapa 4: adicionar runBlockingTest
O erro de corrotina é esperado porque getTasks
é uma função suspend
, e você precisa iniciar uma corrotina para chamá-la. Para isso, você precisa de um escopo de corrotina. Para resolver esse erro, você precisará adicionar algumas dependências do Gradle para processar a inicialização de corrotinas nos seus testes.
- Adicione as dependências necessárias para testar corrotinas ao conjunto de origem de teste usando
testImplementation
.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
Não se esqueça de sincronizar.
kotlinx-coroutines-test
é a biblioteca de testes de corrotinas, criada especificamente para testar corrotinas. Para executar os testes, use a função runBlockingTest
. Esta é uma função fornecida pela biblioteca de testes de corrotinas. Ele usa um bloco de código e o executa em um contexto de corrotina especial que é executado de forma síncrona e imediata, ou seja, as ações ocorrerão em uma ordem determinística. Isso basicamente faz com que as corrotinas sejam executadas como não corrotinas, por isso serve para testar código.
Use runBlockingTest
nas classes de teste ao chamar uma função suspend
. Você aprenderá mais sobre como o runBlockingTest
funciona e como testar corrotinas no próximo codelab desta série.
- Adicione o
@ExperimentalCoroutinesApi
acima da classe. Isso mostra que você sabe que está usando uma API de corrotinas experimental (runBlockingTest
) na classe Sem isso, você receberá um aviso. - De volta ao
DefaultTasksRepositoryTest
, adicionerunBlockingTest
para que ele receba todo o teste como um "bloco" de código
O teste final ficará parecido com o do código abaixo.
DefaultTasksRepositoryTest.kt (link em inglês)
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}
- Execute seu novo teste
getTasks_requestsAllTasksFromRemoteDataSource
e confirme se ele funciona e se o erro desapareceu.
Você acabou de ver como fazer um teste de unidade em um repositório. Nas próximas etapas, você usará novamente a injeção de dependência e criará outro teste duplo, desta vez para mostrar como criar testes de unidade e integração para seus modelos de visualização.
Os testes de unidade testam apenas a classe ou o método em que você tem interesse. Isso é conhecido como teste em isolamento, em que você isola claramente sua "unidade" e testa apenas o código que faz parte dessa unidade.
Portanto, o TasksViewModelTest
precisa testar apenas o código do TasksViewModel
, e não no banco de dados, na rede ou nas classes de repositório. Portanto, para seus modelos de visualização, assim como você fez para o repositório, crie um repositório fictício e aplique a injeção de dependência para usá-lo nos testes.
Nesta tarefa, você aplicará a injeção de dependência para visualizar modelos.
Etapa 1. Criar uma interface do TasksRepository
O primeiro passo para usar a injeção de dependência do construtor é criar uma interface comum compartilhada entre a classe falsa e a real.
Como isso funciona na prática? Analise TasksRemoteDataSource
, TasksLocalDataSource
e FakeDataSource
e observe que todos compartilham a mesma interface: TasksDataSource
. Isso permite que você diga no construtor da DefaultTasksRepository
que você aceita em um TasksDataSource
.
DefaultTasksRepository.kt (link em inglês)
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
É isso que nos permite fazer a troca no FakeDataSource
.
Em seguida, crie uma interface para o DefaultTasksRepository
, da mesma forma que você fez para as fontes de dados. Ela precisa incluir todos os métodos públicos (superfície da API pública) do DefaultTasksRepository
.
- Abra
DefaultTasksRepository
e clique com o botão direito do mouse no nome da classe. Em seguida, selecione Refactor -> Extract -> Interface.
- Escolha Extrair para arquivo separado.
- Na janela Extract Interface, altere o nome da interface para
TasksRepository
. - Na seção Interface "Membros para o formulário", marque todos os participantes exceto os dois participantes complementares e os métodos private.
- Clique em Refactor. A nova interface
TasksRepository
aparecerá no pacote data/source.
Agora, DefaultTasksRepository
implementa TasksRepository
.
- Execute o app (não os testes) para garantir que tudo ainda esteja funcionando.
Etapa 2: Criar FakeTestRepository
Agora que você tem a interface, pode criar o teste DefaultTaskRepository
duplo.
- No conjunto de origem test, em data/source, crie o arquivo e a classe
FakeTestRepository.kt
do Kotlin e estenda a partir da interfaceTasksRepository
.
FakeTestRepository.kt (link em inglês)
class FakeTestRepository : TasksRepository {
}
Você receberá uma mensagem de que precisa implementar os métodos de interface.
- Passe o cursor sobre o erro até ver o menu de sugestões, clique e selecione Implementar membros.
- Selecione todos os métodos e pressione OK.
Etapa 3. Implementar métodos FakeTestRepository
Agora você tem uma classe FakeTestRepository
com métodos "não implementados" Assim como você implementou a FakeDataSource
, a FakeTestRepository
terá uma estrutura de dados em vez de uma mediação complicada entre fontes locais e remotas.
Observe que o FakeTestRepository
não precisa usar FakeDataSource
s nem nada assim. Ele só precisa retornar saídas falsas realistas de acordo com as entradas. Você usará um LinkedHashMap
para armazenar a lista de tarefas e uma MutableLiveData
para tarefas observáveis.
- No
FakeTestRepository
, adicione uma variávelLinkedHashMap
que represente a lista atual de tarefas e umMutableLiveData
para as tarefas observáveis.
FakeTestRepository.kt (link em inglês)
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
Implemente os seguintes métodos:
getTasks
: esse método precisa transformar otasksServiceData
em uma lista usandotasksServiceData.values.toList()
e depois retorná-lo como um resultadoSuccess
.refreshTasks
: atualiza o valor deobservableTasks
para que seja retornado porgetTasks()
.observeTasks
: cria uma corrotina usandorunBlocking
, executarefreshTasks
e retornaobservableTasks
.
Veja abaixo o código desses métodos.
FakeTestRepository.kt (link em inglês)
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
// Rest of class
}
Etapa 4. Adição de um método para teste a addTasks.
Ao testar, é melhor ter alguns Tasks
no seu repositório. Você pode chamar saveTask
várias vezes, mas, para facilitar esse processo, adicione um método auxiliar especificamente para testes que permitem adicionar tarefas.
- Adicione o método
addTasks
, que recebe umavararg
de tarefas, adiciona cada uma àsHashMap
e atualiza as tarefas.
FakeTestRepository.kt (link em inglês)
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
Neste ponto, você tem um repositório fictício para testar com alguns dos principais métodos implementados. Em seguida, use isso nos testes.
Nesta tarefa, você usará uma classe falsa dentro de uma ViewModel
. Use a injeção de dependência do construtor para adicionar as duas fontes de dados com a injeção de dependência do construtor adicionando uma variável TasksRepository
ao construtor do TasksViewModel
.
Esse processo é um pouco diferente dos modelos de visualização porque eles não são criados diretamente. Por exemplo:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
Como no código acima, você está usando a delegação de propriedade do viewModel's
que cria o modelo de visualização. Para mudar a forma como o modelo de visualização é construído, é necessário adicionar e usar uma ViewModelProvider.Factory
. Se você não conhece ViewModelProvider.Factory
, saiba mais sobre ele.
Etapa 1. Criar e usar um ViewModelFactory no TasksViewModel
Comece atualizando as classes e os testes relacionados à tela Tasks
.
- Abra
TasksViewModel
. - Mude o construtor da
TasksViewModel
para receberTasksRepository
em vez de construí-lo dentro da classe.
TasksViewModel.kt.
// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() {
// Rest of class
}
Como você alterou o construtor, agora precisa usar uma fábrica para construir TasksViewModel
. Coloque a classe de fábrica no mesmo arquivo que o TasksViewModel
, mas você também pode colocá-la no próprio arquivo.
- Na parte inferior do arquivo
TasksViewModel
, fora da classe, adicione umaTasksViewModelFactory
que aceita umTasksRepository
simples.
TasksViewModel.kt.
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TasksViewModel(tasksRepository) as T)
}
Esta é a maneira padrão de mudar a forma como ViewModel
s são construídos. Agora que você tem a fábrica, use-a sempre que criar seu modelo de visualização.
- Atualize o
TasksFragment
para usar a fábrica.
TasksFragment.kt.
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Execute o código do app e confira se tudo ainda está funcionando.
Etapa 2: Usar FakeTestRepository no TasksViewModelTest
Agora, em vez de usar o repositório real nos testes de modelos de visualização, você pode usar o repositório fictício.
- Abra o
TasksViewModelTest
. - Adicione uma propriedade
FakeTestRepository
aoTasksViewModelTest
.
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
}
- Atualize o método
setupViewModel
para criar umFakeTestRepository
com três tarefas e, em seguida, construa otasksViewModel
com esse repositório.
TasksViewModelTest.kt (link em inglês)
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeTestRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository)
}
- Como você não está mais usando o código do AndroidX Test
ApplicationProvider.getApplicationContext
, também é possível remover a anotação@RunWith(AndroidJUnit4::class)
. - Execute seus testes e confira se todos eles ainda funcionam.
Ao usar a injeção de dependência do construtor, você removeu o DefaultTasksRepository
como uma dependência e o substituiu pela sua FakeTestRepository
nos testes.
Etapa 3. Atualizar também o Fragment TaskDetail e o ViewModel
Faça as mesmas mudanças em TaskDetailFragment
e TaskDetailViewModel
. Isso preparará o código para quando você criar testes do TaskDetail
em seguida.
- Abra
TaskDetailViewModel
. - 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 }
- Na parte inferior do arquivo
TaskDetailViewModel
, fora da classe, adicione umTaskDetailViewModelFactory
.
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)
}
- Atualize o
TasksFragment
para usar a fábrica.
TasksFragment.kt.
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Execute o código e confira se tudo está funcionando.
Agora é possível usar um FakeTestRepository
em vez do repositório real em TasksFragment
e TasksDetailFragment
Em seguida, você criará testes de integração para testar o fragmento e as interações de modelo de visualização. Você descobrirá se o código do modelo de visualização atualiza a IU corretamente. Para fazer isso, você usa
- o padrão ServiceLocator
- as bibliotecas Espresso e Mockito
Os testes de integração analisam as interações de várias classes para verificar se elas se comportam conforme o esperado quando usadas juntas. Esses testes podem ser executados localmente (conjunto de origem test
) ou como testes de instrumentação (conjunto de origem androidTest
).
No seu caso, você usará cada fragmento e criará testes de integração para o fragmento e o modelo de visualização para testar os principais recursos.
Etapa 1. Adicionar dependências do Gradle
- Adicione as dependências do Gradle a seguir.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
Essas dependências incluem o seguinte:
junit:junit
: JUnit, que é necessário para escrever instruções de teste básicas.androidx.test:core
: biblioteca de teste principal do AndroidXkotlinx-coroutines-test
: a biblioteca de testes de corrotinasandroidx.fragment:fragment-testing
: biblioteca de teste AndroidX para criar fragmentos em testes e mudar seu estado.
Como você usará essas bibliotecas no seu conjunto de origem androidTest
, use androidTestImplementation
para adicioná-las como dependências.
Etapa 2: Criar uma classe TaskDetailFragmentTest
O TaskDetailFragment
mostra informações sobre uma única tarefa.
Você começará escrevendo um teste de fragmento para o TaskDetailFragment
, já que ele tem as funcionalidades básicas em comparação com os outros fragmentos.
- Abra
taskdetail.TaskDetailFragment
. - Gere um teste de
TaskDetailFragment
, como você fez anteriormente. Aceite as opções padrão e coloque-as no conjunto de origem androidTest (NÃO no conjunto de origem detest
).
- Adicione as seguintes anotações à classe
TaskDetailFragmentTest
.
TaskDetailFragmentTest.kt.
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
O objetivo dessa anotação é:
@MediumTest
: marca o teste como um "teste de integração" de tempo médio (em vez de testes de unidade do@SmallTest
e@LargeTest
grandes testes de ponta a ponta). Isso ajuda você a agrupar e escolher o tamanho do teste a ser executado.@RunWith(AndroidJUnit4::class)
: usado em qualquer classe com o AndroidX Test.
Etapa 3. Iniciar um fragmento de um teste
Nesta tarefa, você iniciará o TaskDetailFragment
usando a biblioteca AndroidX Test. FragmentScenario
é uma classe do AndroidX Test que envolve um fragmento e oferece controle direto sobre o ciclo de vida do fragmento para testes. Para programar testes de fragmentos, crie uma classe FragmentScenario
para o fragmento que você está testando (TaskDetailFragment
).
- Copie este teste para
TaskDetailFragmentTest
.
TaskDetailFragmentTest.kt.
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
O código acima:
- Cria uma tarefa.
- Cria um
Bundle
, que representa os argumentos do fragmento para a tarefa que são transmitidos para o fragmento. - A função
launchFragmentInContainer
cria umaFragmentScenario
com esse pacote e um tema.
Este teste ainda não foi concluído, porque não declara nada. Por enquanto, execute o teste e veja o que acontece.
- Esse é um teste instrumentado. Portanto, confira se o emulador ou seu dispositivo está visível.
- Execute o teste.
Algumas coisas acontecerão.
- Primeiro, como esse é um teste de instrumentação, ele será executado no dispositivo físico (se conectado) ou em um emulador.
- Ele iniciará o fragmento.
- Observe como ele não navega por nenhum outro fragmento nem tem menus associados à atividade. É apenas o fragmento.
Por fim, observe atentamente e observe que o fragmento diz "Não há dados", já que ele não carrega os dados da tarefa com sucesso.
O teste precisa carregar o TaskDetailFragment
(que você fez) e declarar que os dados foram carregados corretamente. Por que não há dados? Isso ocorreu porque você criou uma tarefa, mas não a salvou no repositório.
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
Você tem esse FakeTestRepository
, mas precisa de uma maneira de substituir o repositório real pelo repositório falso do seu fragmento. Você fará isso a seguir.
Nesta tarefa, você fornecerá seu repositório fictício para seu fragmento usando um ServiceLocator
. Isso permitirá que você grave seu fragmento e veja os testes de integração do modelo.
Não é possível usar a injeção de dependência do construtor aqui, como você fez anteriormente, quando precisava fornecer uma dependência ao modelo de visualização ou repositório. A injeção de dependência do construtor exige que você construa a classe. Fragmentos e atividades são exemplos de classes que você não cria e geralmente não tem acesso ao construtor.
Como você não constrói o fragmento, não é possível usar a injeção de dependência do construtor para trocar o teste do repositório duplo (FakeTestRepository
) pelo fragmento. Em vez disso, use o padrão Service Locator. O padrão do localizador de serviços é uma alternativa à injeção de dependência. Isso envolve a criação de uma classe Singleton chamada "Service Locator", com o objetivo de fornecer dependências para o código normal e de teste. No código normal do app (o conjunto de origem main
), todas essas dependências são as dependências regulares do app. Para os testes, modifique o localizador de serviço para fornecer versões duplas de teste das dependências.
Não usa o localizador de serviços | Como usar um localizador de serviços |
Para este app de codelab, faça o seguinte:
- Crie uma classe de localizador de serviços capaz de construir e armazenar um repositório. Por padrão, ele cria um repositório "normal" .
- Refatorar o código para usar o Localizador de serviços quando necessário.
- Na classe de teste, chame um método no Service Locator, que troca o repositório "normal" pelo duplo de teste.
Etapa 1. Criar o ServiceLocator
Vamos criar uma classe ServiceLocator
. Ele será inserido no conjunto de origem principal com o restante do código do app porque é usado pelo código do aplicativo principal.
Observação: o ServiceLocator
é um Singleton. Portanto, use a palavra-chave object
do Kotlin (link em inglês) para a classe.
- Crie o arquivo ServiceLocator.kt no nível superior do conjunto de origem principal.
- Defina um
object
com o nomeServiceLocator
. - Crie as variáveis de instância
database
erepository
e defina ambas comonull
. - Anote o repositório com
@Volatile
, porque ele pode ser usado por várias linhas de execução (@Volatile
é explicado em detalhes aqui).
O código ficará assim:
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
No momento, a única coisa que o ServiceLocator
precisa fazer é saber como retornar um TasksRepository
. Ele retornará um DefaultTasksRepository
preexistente ou criará e retornará um novo DefaultTasksRepository
, se necessário.
Defina as seguintes funções:
provideTasksRepository
: um repositório já existente ou um novo é criado. Esse método precisa sersynchronized
emthis
para evitar, em situações com várias linhas de execução em execução, acidentalmente criando duas instâncias de repositório.createTasksRepository
: código para criar um novo repositório. Ele chamarácreateTaskLocalDataSource
e criará um novoTasksRemoteDataSource
.createTaskLocalDataSource
: código para criar uma nova fonte de dados local. ChamarácreateDataBase
.createDataBase
: código para criar um novo banco de dados.
Veja a seguir o código completo.
ServiceLocator.kt (em inglês)
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}
Etapa 2: Usar o ServiceLocator no aplicativo
Você fará uma alteração no código principal do seu aplicativo (não nos testes) para criar o repositório em um só lugar, o ServiceLocator
.
É importante que você crie apenas uma instância da classe de repositório. Para garantir isso, você usará o localizador de serviço na minha classe de aplicativo.
- No nível superior da hierarquia do pacote, abra
TodoApplication
e crie umval
para o repositório e atribua a ele um repositório recebido usandoServiceLocator.provideTaskRepository
.
TodoApplication.kt.
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
Agora que você criou um repositório no aplicativo, é possível remover o antigo método getRepository
no DefaultTasksRepository
.
- Abra
DefaultTasksRepository
e exclua o objeto complementar.
DefaultTasksRepository.kt (link em inglês)
// DELETE THIS COMPANION OBJECT
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}
Agora, em qualquer lugar em que você usava getRepository
, use o taskRepository
do aplicativo. Isso garante que, em vez de fazer o repositório diretamente, você receba o repositório fornecido pelo ServiceLocator
.
- Abra
TaskDetailFragement
e encontre a chamada paragetRepository
na parte superior da classe. - 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)
}
- 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)
}
- Para
StatisticsViewModel
eAddEditTaskViewModel
, atualize o código que adquire o repositório para usar o repositório doTodoApplication
.
TasksFragment.kt.
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- Execute o aplicativo (não o teste).
Como você só refatorou o app, ele deve ser executado sem problemas.
Etapa 3. Criar FakeAndroidTestRepository
Você já tem um FakeTestRepository
no conjunto de origem de teste. Por padrão, não é possível compartilhar classes de teste entre os conjuntos de origem test
e androidTest
. Portanto, você precisa criar uma classe FakeTestRepository
duplicada no conjunto de origem androidTest
e chamá-la de FakeAndroidTestRepository
.
- Clique com o botão direito do mouse no conjunto de origem
androidTest
e crie um pacote de dados. Clique com o botão direito novamente e crie um pacote source. - Crie uma nova classe neste pacote de origem com o nome
FakeAndroidTestRepository.kt
. - Copie o seguinte código para essa classe.
FakeAndroidTestRepository.kt (link em inglês)
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
class FakeAndroidTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
Etapa 4. Preparar seu ServiceLocator para testes
Ok, é hora de usar a ServiceLocator
para trocar o dobro no teste. Para fazer isso, você precisa adicionar um código ao ServiceLocator
.
- Abra
ServiceLocator.kt
. - Marque o setter para
tasksRepository
como@VisibleForTesting
. Essa anotação é uma maneira de expressar que o motivo da razão pública é o teste.
ServiceLocator.kt (em inglês)
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Não importa se você executa o teste sozinho ou em um grupo de testes, eles precisam ser executados exatamente da mesma forma. Isso significa que os seus testes não devem ter nenhum comportamento dependente um do outro (o que significa evitar o compartilhamento de objetos entre testes).
Como a ServiceLocator
é um Singleton, ela pode ser compartilhada acidentalmente entre os testes. Para ajudar a evitar isso, crie um método que redefina corretamente o estado ServiceLocator
entre os testes.
- Adicione uma variável de instância chamada
lock
com o valorAny
.
ServiceLocator.kt (em inglês)
private val lock = Any()
- Adicione um método específico de teste chamado
resetRepository
, que limpa o banco de dados e define o repositório e o banco de dados como nulo.
ServiceLocator.kt (em inglês)
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}
Etapa 5: Usar o ServiceLocator
Nesta etapa, você usa o ServiceLocator
.
- Abra
TaskDetailFragmentTest
. - Declare uma variável
lateinit TasksRepository
. - Adicione uma configuração e um método de desmontagem para configurar uma
FakeAndroidTestRepository
antes de cada teste e limpá-la após cada teste.
TaskDetailFragmentTest.kt.
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Una o corpo da função de
activeTaskDetails_DisplayedInUi()
emrunBlockingTest
. - Salve
activeTask
no repositório antes de iniciar o fragmento.
repository.saveTask(activeTask)
O teste final ficará assim:
TaskDetailFragmentTest.kt.
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
- Adicione a anotação
@ExperimentalCoroutinesApi
à classe inteira.
Quando terminar, o código ficará assim.
TaskDetailFragmentTest.kt.
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
- Execute o teste
activeTaskDetails_DisplayedInUi()
.
Assim como antes, você verá o fragmento, exceto que, agora que configurou o repositório corretamente, ele mostrará as informações da tarefa.
Nesta etapa, você usará a biblioteca de testes da IU do Espresso para concluir seu primeiro teste de integração. Você estruturau o código para poder adicionar testes com declarações para a IU. Para fazer isso, você usará a biblioteca de testes Espresso.
O Espresso ajuda você a:
- Interaja com visualizações, como clicar em botões, deslizar uma barra ou rolar uma tela para baixo.
- Declarar que determinadas visualizações estão na tela ou estão em determinado estado (por exemplo, com texto específico ou com uma caixa de seleção marcada etc.)
Etapa 1. Observação da dependência do Gradle
Você já terá a dependência principal do Espresso, já que ela é incluída em projetos Android por padrão.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}
androidx.test.espresso:espresso-core
: essa dependência principal do Espresso é incluída por padrão quando você cria um novo projeto Android. Ele contém o código de teste básico para a maioria das visualizações e ações relacionadas.
Etapa 2: Desativar animações
Os testes do Espresso são executados em um dispositivo real e, portanto, são testes de instrumentação por natureza. Um problema que acontece são as animações: se uma animação trava e você tenta testar se uma visualização está na tela, mas ela ainda está sendo animada, o Espresso pode falhar acidentalmente em um teste. Isso pode tornar os testes do Espresso instáveis.
Para os testes de IU do Espresso, é recomendável desativar as animações. O teste será executado mais rapidamente.
- No dispositivo de teste, acesse Configurações > Opções do desenvolvedor.
- Desative estas três configurações: Escala de animação da janela, Escala de animação da transição e Escala de duração do animador.
Etapa 3. Ver um teste do Espresso
Antes de programar um teste do Espresso, confira alguns códigos do Espresso.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
Essa instrução encontra a visualização da caixa de seleção com o ID task_detail_complete_checkbox
, clica nela e declara que está marcada.
A maioria das instruções do Espresso são compostas por quatro partes:
1. Método estático do Espresso
onView
onView
é um exemplo de um método estático do Espresso que inicia uma instrução do Espresso. onView
é uma das mais comuns, mas existem outras opções, como onData
.
2. ViewMatcher.
withId(R.id.task_detail_title_text)
withId
é um exemplo de ViewMatcher
que recebe uma visualização pelo ID. Há outros correspondentes de visualização que podem ser consultados na documentação.
3. ViewAction
perform(click())
O método perform
que usa um ViewAction
. Um ViewAction
pode ser usado para visualizar a página, por exemplo, quando ele clica na visualização.
4. ViewAssertion.
check(matches(isChecked()))
check
, que usa um ViewAssertion
. ViewAssertion
s verificam ou declaram algo sobre a visualização. A ViewAssertion
mais comum que você usará é a declaração matches
. Para concluir a declaração, use outro ViewMatcher
, neste caso isChecked
.
Observe que você nem sempre chama perform
e check
em uma instrução Espresso. É possível ter instruções que apenas fazem uma declaração usando check
ou apenas um ViewAction
usando perform
.
- Abra
TaskDetailFragmentTest.kt
. - Atualize o teste
activeTaskDetails_DisplayedInUi
.
TaskDetailFragmentTest.kt.
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
Veja as instruções de importação, se necessário:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
- Tudo o que aparece depois do comentário
// THEN
usa o Espresso. Analise a estrutura de teste e o uso dewithId
e confira se há declarações sobre a aparência da página de detalhes. - Execute o teste e confirme se ele foi aprovado.
Etapa 4. Opcional: criar seu próprio teste do Espresso
Agora, crie um teste.
- Crie um novo teste chamado
completedTaskDetails_DisplayedInUi
e copie esse código esqueleto.
TaskDetailFragmentTest.kt.
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}
- Analisando o teste anterior, conclua este teste.
- Execute e confirme a aprovação do teste.
A completedTaskDetails_DisplayedInUi
concluída ficará parecida com este código.
TaskDetailFragmentTest.kt.
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}
Na última etapa, você aprenderá a testar o componente de navegação usando um tipo diferente de teste duplo chamado simulação e a biblioteca de teste Mockito.
Neste codelab, você usou um teste duplo chamado falso. As falsificações são um dos muitos tipos de cópias de teste. Qual teste duplo você deve usar para testar o componente de navegação?
Pense em como a navegação acontece. Imagine pressionar uma das tarefas no TasksFragment
para navegar até uma tela de detalhes da tarefa.
Este código em TasksFragment
, que navega para uma tela detalhada de tarefas quando é pressionado.
TasksFragment.kt.
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
A navegação ocorre devido a uma chamada para o método navigate
. Caso seja necessário escrever uma declaração de declaração, não há uma maneira direta de testar se você navegou para TaskDetailFragment
. Navegar é uma ação complicada que não resulta em uma saída ou mudança de estado clara, além de inicializar TaskDetailFragment
.
É possível declarar que o método navigate
foi chamado com o parâmetro de ação correto. Isso é exatamente o que um teste de simulação faz e verifica se métodos específicos foram chamados.
O Mockito é um framework para criar testes duplos. Embora a palavra simulação seja usada na API e no nome, ela não serve para fazer simulações. Também pode criar stubs e espiões.
Você usará o Mockito para criar uma NavigationController
fictícia, que pode declarar que o método de navegação foi chamado corretamente.
Etapa 1. Adicionar dependências do Gradle
- Adicione as dependências do Gradle.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
org.mockito:mockito-core
: é a dependência do Mockito.dexmaker-mockito
: esta biblioteca é necessária para usar o Mockito em um projeto Android. O Mockito precisa gerar classes no momento da execução. No Android, isso é feito usando o código de bytes dex. Portanto, essa biblioteca permite que o Mockito gere objetos durante o tempo de execução no Android.androidx.test.espresso:espresso-contrib
: essa biblioteca é composta por contribuições externas, que têm o nome de um código de teste para visualizações mais avançadas, comoDatePicker
eRecyclerView
. Ele também contém as verificações de acessibilidade e a classeCountingIdlingResource
, que serão abordadas mais tarde.
Etapa 2: Criar TasksFragmentTest
- Abra o
TasksFragment
- Clique com o botão direito do mouse no nome da classe
TasksFragment
e selecione Gerar e Testar. Crie um teste no conjunto de origem androidTest. - Copie esse código para o
TasksFragmentTest
.
TasksFragmentTest.kt.
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
}
Esse código é semelhante ao código TaskDetailFragmentTest
que você escreveu. Ele configura e desmonta um FakeAndroidTestRepository
. Adicione um teste de navegação para verificar se, ao clicar em uma tarefa na lista de tarefas, você acessa o TaskDetailFragment
correto.
- 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)
}
- Use a função
mock
do Mockitoc para criar uma simulação.
TasksFragmentTest.kt.
val navController = mock(NavController::class.java)
Para simular no Mockito, transmita a classe que você quer simular.
Em seguida, você precisa associar a NavController
ao fragmento. onFragment
permite que você chame métodos no próprio fragmento.
- Faça a nova simulação do
NavController
do fragmento.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- Adicione o código para clicar no item no
RecyclerView
que tem o texto"quot;TITLE1"".
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
O RecyclerViewActions
faz parte da biblioteca espresso-contrib
e permite executar ações do Espresso em um RecyclerView.
- Verifique se
navigate
foi chamado com o argumento correto.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
O método verify
do Mockito gera o teste. Você pode confirmar quando um navController
simulado é chamado de um método específico (navigate
) com um parâmetro (actionTasksFragmentToTaskDetailFragment
com o ID de "id1").
O teste completo fica assim:
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}
- Execute o teste.
Em resumo, para testar a navegação, você pode:
- Usar o Mockito para criar uma simulação
NavController
- Anexe essa
NavController
simulada ao fragmento. - Verifique se a navegação foi chamada com a ação e os parâmetros corretos.
Etapa 3. Opcional: escreva clickAddTaskButton_navigationToAddEditFragment
Para ver se você pode criar um teste de navegação, tente realizar esta tarefa.
- Escreva o teste
clickAddTaskButton_navigateToAddEditFragment
, que verifica se você clica no + FAB e navega para oAddEditTaskFragment
.
A resposta está abaixo.
TasksFragmentTest.kt.
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}
Clique aqui para ver uma diferença entre o código iniciado e o código final.
Para fazer o download do código do codelab concluído, use o comando git abaixo:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
Como alternativa, é possível fazer o download do repositório como um arquivo ZIP, descompactá-lo e abri-lo no Android Studio.
Este codelab ensinou como configurar a injeção de dependência manual, um localizador de serviços e como usar simulações e simulações nos seus apps Kotlin para Android. Especificamente, as seguintes:
- O que você quer testar e a estratégia de teste determina os tipos de teste que você implementará no app. Os testes de unidade são focados e rápidos. Os testes de integração confirmam a interação entre partes do programa. Os testes completos verificam os recursos, têm a maior fidelidade, costumam ser instrumentados e podem demorar mais para serem executados.
- A arquitetura do app influencia a dificuldade de teste.
- TDD ou desenvolvimento orientado por testes é uma estratégia em que você programa os testes primeiro e depois cria o recurso para passar nos testes.
- Para isolar partes do app para testes, use testes duplos. Um teste duplo é uma versão de uma classe criada especificamente para testes. Por exemplo, você finge receber dados de um banco de dados ou da Internet.
- Use a injeção de dependência para substituir uma classe real por uma classe de teste, por exemplo, um repositório ou uma camada de rede.
- Use testes ilustrados (
androidTest
) para iniciar os componentes da IU. - Quando não é possível usar a injeção de dependência do construtor, por exemplo, para iniciar um fragmento, você pode usar um localizador de serviços. O padrão do localizador de serviços é uma alternativa à injeção de dependência. Isso envolve a criação de uma classe Singleton chamada "Service Locator", com o objetivo de fornecer dependências para o código normal e de teste.
Curso da Udacity:
- Como desenvolver apps Android com Kotlin (link em inglês)
Documentação do desenvolvedor Android:
- Guia para a arquitetura do app
runBlocking
erunBlockingTest
FragmentScenario
- Espresso
- Mockito (em inglês)
- JUnit4.
- Biblioteca AndroidX Test
- Biblioteca de testes principais dos Componentes da arquitetura do AndroidX
- Conjuntos de origem
- Testar na linha de comando
Vídeos:
Outro:
Para ver links de outros codelabs neste curso, consulte a página de destino dos codelabs avançados no Android.