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.
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:
Você deve estar familiarizado com:
ViewModel
, LiveData
e o Componente de navegação
Você usará as seguintes bibliotecas e conceitos de código:
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.
Para começar, baixe o código:
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.
Depois de baixar o aplicativo TO-DO, abra-o no Android Studio e execute-o. Deve compilar. Explore o aplicativo fazendo o seguinte:
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: |
|
|
The add or edit a task screen: código da camada de IU para adicionar ou editar uma tarefa. |
|
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. |
|
A tela de estatística: Código da camada de IU para a tela de estatísticas. |
|
A tela de detalhe de tarefa: código da camada de IU para uma única tarefa. |
|
A tela de tarefas: código da camada de IU para a lista de todas as tarefas. |
|
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.
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:
test
). Example: Teste de métodos únicos em modelos de vista e repositórios.androidTest
) A proporção sugerida desses testes é frequentemente representada por uma pirâmide, com a grande maioria dos testes sendo testes unitários.
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:
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:
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 |
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 |
Espião |
Um dublê de teste que também rastreia algumas informações adicionais; por exemplo, se você fez um |
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.
Nesta etapa, você criará uma classe chamada FakeDataSouce
, que será um teste dublê de LocalDataSource
e RemoteDataSource
.
FakeDataSource
no pacote data/source.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
.
TasksDataSource
.class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
FakeDataSource
implementar TasksDataSource
:class FakeDataSource : TasksDataSource {
}
O Android Studio reclamará que você não implementou os métodos necessários para TasksDataSource
.
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
DefaultTasksRepository
sem a necessidade de contar com um banco de dados ou rede realFakeDataSource
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:
getTasks
: Se tasks
não for null
, retorne um resultado de Success
. Se tasks
for null
, retorne um resultado de Error
.deleteAllTasks
: limpe a lista de tarefas mutáveis.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 |
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 }
init
. Você não precisa mais criar as dependências.DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
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!
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
.
DefaultTasksRepository
e selecione Generate e, em seguida, Test.DefaultTasksRepositoryTest
no conjunto de origem test.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 }
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
.
createRepository
e anote-o com @Before
.remoteTasks
e localTasks
.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
)
}
É hora de escrever um teste DefaultTasksRepository
!
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:
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.
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.
@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.DefaultTasksRepositoryTest
, adicione runBlockingTest
para que ele leve todo o seu teste como um "bloco" de códigoEste 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))
}
}
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.
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
.
DefaultTasksRepository
e clique com botão da direita no nome da classe. Em seguida, selecione Refactor -> Extract -> Interface.TasksRepository
.TasksRepository
deve aparecer no pacote data/source.E DefaultTasksRepository
agora implementa TasksRepository
.
Agora que você tem a interface, pode criar o teste dublê DefaultTaskRepository
.
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.
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.
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:
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
.refreshTasks
—Atualiza o valor de observableTasks
para ser o que é retornado por getTasks()
.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
}
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.
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.
Você começa atualizando as classes e testes relacionados à tela Tasks
.
TasksViewModel
.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.
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.
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))
}
Agora, em vez de usar o repositório real em seus testes de modelo de vista, você pode usar o repositório falso.
TasksViewModelTest
.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
}
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)
}
ApplicationProvider.getApplicationContext
, também pode remover o código @RunWith(AndroidJUnit4::class)
anotação.Usando injeção de dependência de construtor, você removeu o DefaultTasksRepository
como uma dependência e o substituiu por seu FakeTestRepository
nos testes.
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.
TaskDetailViewModel
.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 }
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)
}
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))
}
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.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
Essas dependências incluem:
junit:junit
—JUnit, que é necessário para escrever instruções de teste básicas.androidx.test:core
- Biblioteca de teste Core AndroidXkotlinx-coroutines-test
—A biblioteca de teste de corrotinas androidx.fragment:fragment-testing
—Biblioteca de teste AndroidX para criar fragmentos em testes e alterar seu estado.Como você usará essas bibliotecas em seu conjunto de origem androidTest
, use androidTestImplementation
para adicioná-las como dependências.
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.
taskdetail.TaskDetailFragment
.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
).TaskDetailFragmentTest
.TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
O objetivo dessas anotações é:
@MediumTest
—Marca o teste como um teste de integração de "tempo de execução médio" (versus @SmallTest
testes unitários e @LargeTest
testes ponta a ponta). Isso ajuda a agrupar e escolher o tamanho do teste a ser executado.@RunWith(AndroidJUnit4::class)
—Usado em qualquer classe usando o teste AndroidX.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
).
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:
Bundle
, que representa os argumentos do fragmento para a tarefa que são passados para o fragmento).launchFragmentInContainer
function creates a
FragmentScenario
, with this
bundle and a theme. Este ainda não é um teste concluído, porque não está afirmando nada. Por enquanto, execute o teste e observe o que acontece.
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:
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.
object
chamado ServiceLocator
.database
e repository
e defina ambas como null
.@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:
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.
createTasksRepository
—Código para criar um novo repositório. Chamará createTaskLocalDataSource
e criará um novo TasksRemoteDataSource
.createTaskLocalDataSource
—Código para criar uma nova fonte de dados local. Chamará createDataBase
.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
}
}
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.
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
.
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.
TaskDetailFragement
e encontre a chamada para getRepository
no início da classe. 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)
}
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)
}
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
Como você apenas refatorou, o aplicativo deve ser executado da mesma forma sem problemas.
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
.
androidTest
e crie um pacote de data. Clique com o botão direito novamente e crie um pacote source. FakeAndroidTestRepository.kt
.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() }
}
}
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
.
ServiceLocator.kt
.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.
lock
com o valor Any
.ServiceLocator.kt
private val lock = Any()
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
}
}
Nesta etapa, você usa o ServiceLocator
.
TaskDetailFragmentTest
.lateinit TasksRepository
.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()
}
activeTaskDetails_DisplayedInUi()
em runBlockingTest
.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)
}
@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)
}
}
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:
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.
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!):
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:
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.
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
.
TaskDetailFragmentTest.kt
.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
// 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.Agora escreva um teste você mesmo.
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
}
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.
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
—Esta é a dependência Mockito.dexmaker-mockito
—Esta biblioteca é necessária para usar o Mockito em um projeto Android. O Mockito precisa gerar classes em tempo de execução. No Android, isso é feito usando o código de byte dex e, portanto, esta biblioteca permite que o Mockito gere objetos durante o tempo de execução no Android.androidx.test.espresso:espresso-contrib
—Esta biblioteca é composta de contribuições externas que contêm código de teste para vistas mais avançadas, como DatePicker
e RecyclerView
. Ele também contém verificações de acessibilidade e uma classe chamada CountingIdlingResource
que é abordada posteriormente.TasksFragment
.TasksFragment
e selecione Generate e depois Test. Crie um teste no conjunto de origem androidTest.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.
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)
}
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.
NavController
do fragmento.scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
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.
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")
)
}
Em resumo, para testar a navegação, você pode:
NavController
.NavController
simulado ao fragmento.Para ver se você mesmo pode escrever um teste de navegação, tente esta tarefa.
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.
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:
androidTest
) para iniciar os componentes da IU. Amostras:
Documentação do desenvolvedor Android:
runBlocking
e runBlockingTest
FragmentScenario
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.