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.
Quando você implementou o primeiro recurso de seu primeiro aplicativo, provavelmente executou o código para verificar se funcionava conforme o esperado. Você realizou um teste, embora seja um teste manual. À medida que continuou a adicionar e atualizar recursos, provavelmente também continuou a executar seu código e a verificar se ele funciona. Mas fazer isso manualmente todas as vezes é cansativo, sujeito a erros e não tem escala.
Os computadores são ótimos em dimensionamento e automação! Portanto, os desenvolvedores em grandes e pequenas empresas escrevem automated tests, que são testes executados por software e não exigem que você opere manualmente o aplicativo para verificar se o código funciona.
O que você aprenderá nesta série de tutoriais é como criar uma coleção de testes (conhecida como conjunto de teste) para um aplicativo do mundo real.
Este primeiro tutorial cobre os conceitos básicos de teste no Android, você escreverá seus primeiros testes e aprenderá como testar LiveData
e ViewModel
s.
Você deve estar familiarizado com:
ViewModel
e LiveData
Você aprenderá sobre os seguintes tópicos:
Você aprenderá sobre as seguintes bibliotecas e conceitos de código:
LiveData
e ViewModel
simples.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 várias 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 starter_code
Você pode navegar pelo código no android-testing Github repository.
Nesta tarefa, você executará o aplicativo e explorará a base de código.
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.
Nesta tarefa, você executará seus primeiros testes.
com.example.android.architecture.blueprints.todoapp
com.example.android.architecture.blueprints.todoapp (androidTest)
com.example.android.architecture.blueprints.todoapp (test)
Essas pastas são conhecidas como source sets. Os conjuntos de fontes são pastas que contêm o código-fonte do seu aplicativo. Os conjuntos de origem, que são coloridos em verde (androidTest e test), contêm seus testes. Ao criar um novo projeto Android, você obtém os três conjuntos de fontes a seguir por padrão. Eles são:
main
: Contém o código do seu aplicativo. Este código é compartilhado entre todas as diferentes versões do aplicativo que você pode construir (conhecido como variantes de construção)androidTest
: Contém testes conhecidos como testes instrumentados.test
: Contém testes conhecidos como testes locais.A diferença entre testes locais e testes instrumentados está na maneira como são executados.
Testes locais (teste
conjunto de origem)???
Esses testes são executados localmente na JVM de sua máquina de desenvolvimento e não requerem um emulador ou dispositivo físico. Por causa disso, eles correm rápido, mas sua fidelidade é menor, o que significa que eles agem menos como fariam no mundo real.
No Android Studio, os testes locais são representados por um ícone de triângulo verde e vermelho.
Testes instrumentados (androidTest
conjunto de origem)???
Esses testes são executados em dispositivos Android reais ou emulados, portanto, eles refletem o que acontecerá no mundo real, mas também são muito mais lentos.
No Android Studio, os testes instrumentados são representados por um Android com um ícone de triângulo verde e vermelho.
test
até encontrar o arquivo ExampleUnitTest.kt. Você deve ver a seguinte saída na janela Run na parte inferior da tela:
addition_isCorrect
passou. É bom saber que a adição funciona conforme o esperado!Abaixo está o teste que você acabou de executar.
ExampleUnitTest.kt
// A test class is just a normal class
class ExampleUnitTest {
// Each test is annotated with @Test (this is a Junit annotation)
@Test
fun addition_isCorrect() {
// Here you are checking that 4 is the same as 2+2
assertEquals(4, 2 + 2)
}
}
Observe que os testes
@Test
(cada função é um único teste).O Android usa a biblioteca de teste JUnit para teste (neste tutorial JUnit4). Ambas, as asserções e a anotação @Test
vêm do JUnit.
Uma asserção é o núcleo do seu teste. É uma instrução de código que verifica se seu código ou aplicativo se comportou conforme o esperado. Nesse caso, a asserção é assertEquals(4, 2 + 2)
que verifica se 4 é igual a 2 + 2.
Para ver a aparência de um teste com falha, adicione uma afirmação que você pode facilmente ver que deve falhar. Ele verificará se 3 é igual a 1 + 1.
assertEquals(3, 1 + 1)
ao teste addition_isCorrect
.ExampleUnitTest.kt
class ExampleUnitTest {
// Each test is annotated with @Test (this is a Junit annotation)
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
assertEquals(3, 1 + 1) // This should fail
}
}
(ExampleUnitTest.kt:16)
.Os testes instrumentados estão no conjunto de origem androidTest
.
androidTest
.ExampleInstrumentedTest
.ExampleInstrumentedTest
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.android.architecture.blueprints.reactive",
appContext.packageName)
}
}
Ao contrário do teste local, este teste é executado em um dispositivo (no exemplo abaixo, um smartphone Pixel 2 emulado):
Se você tiver um dispositivo conectado ou um emulador em execução, deverá ver o teste executado no emulador.
Nesta tarefa, você escreverá testes para getActiveAndCompleteStats
, que calcula a porcentagem de estatísticas de tarefas ativas e completas para seu aplicativo. Você pode ver esses números na tela de estatísticas do aplicativo.
main
, em todoapp.statistics
, abra StatisticsUtils.kt
. getActiveAndCompletedStats
.StatisticsUtils.kt
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
val activePercent = 100 * numberOfActiveTasks / totalTasks
val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks
return StatsResult(
activeTasksPercent = activePercent.toFloat(),
completedTasksPercent = completePercent.toFloat()
)
}
data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)
A função getActiveAndCompletedStats
aceita uma lista de tarefas e retorna um StatsResult
. StatsResult
é uma classe de dados que contém dois números, a porcentagem de tarefas que estão concluídas e a porcentagem que estão ativas.
O Android Studio oferece ferramentas para gerar esboços de teste e ajudá-lo a implementar os testes dessa função.
getActiveAndCompletedStats
e selecione Generate> Test.A caixa de diálogo Create Test é aberta:
StatisticsUtilsTest
(em vez de StatisticsUtilsKtTest
; é um pouco melhor não ter KT no nome da classe de teste).
StatisticsUtils
) e você não precisa marcar nenhuma das caixas de seleção (isso apenas gera código extra, mas você escreverá seu teste a partir de coçar, arranhão).A caixa de diálogo Choose Destination Directory é aberta:
Você fará um teste local porque sua função está fazendo cálculos matemáticos e não incluirá nenhum código específico do Android. Portanto, não há necessidade de executá-lo em um dispositivo real ou emulado.
test
(não androidTest
) porque você escreverá testes locais.StatisticsUtilsTest
em test/statistics/
.Você vai escrever um teste que verifica:
StatisticsUtilsTest
.getActiveAndCompletedStats_noCompleted_returnsHundredZero
.StatisticsUtilsTest.kt
class StatisticsUtilsTest {
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task
// Call your function
// Check the result
}
}
@Test
acima do nome da função para indicar que é um teste.// Create an active task
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
getActiveAndCompletedStats
com essas tarefas.// Call your function
val result = getActiveAndCompletedStats(tasks)
result
é o que você esperava, usando asserções.// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
Aqui está o código completo.
StatisticsUtilsTest.kt
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task (the false makes this active)
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// Call your function
val result = getActiveAndCompletedStats(tasks)
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
}
}
StatisticsUtilsTest
e selecione Run).Deve passar:
Como seus testes atuam como documentação do que seu código faz, é bom quando eles podem ser lidos por humanos. Compare as duas afirmações a seguir:
assertEquals(result.completedTasksPercent, 0f)
// versus
assertThat(result.completedTasksPercent, `is`(0f))
A segunda afirmação parece muito mais com uma frase humana. Ele é escrito usando uma estrutura de asserção chamada Hamcrest. Outra boa ferramenta para escrever asserções legíveis é a Biblioteca Truth. Você usará o Hamcrest neste tutorial para escrever asserções.
build.grade (Module: app)
e adicione a seguinte dependência.app/build.gradle
dependencies {
// Other dependencies
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}
Normalmente, você usa implementation
ao adicionar uma dependência, mas aqui você está usando testImplementation
. Quando você estiver pronto para compartilhar seu aplicativo com o mundo, é melhor não aumentar o tamanho do seu APK com nenhum código de teste ou dependências em seu aplicativo. Você pode designar se uma biblioteca deve ser incluída no código principal ou de teste usando configurações do gradle. As configurações mais comuns são:
implementation
—A dependência está disponível em todos os conjuntos de origem, incluindo os conjuntos de origem de teste.testImplementation
—A dependência somente está disponível no conjunto de origem de teste.androidTestImplementation
—A dependência somente está disponível no conjunto de origem androidTest
.A configuração que você usa define onde a dependência pode ser usada. Se você escrever:
testImplementation "org.hamcrest: hamcrest-all: $ hamcrestVersion"
Isso significa que o Hamcrest estará disponível apenas no conjunto de fonte de teste. Também garante que o Hamcrest não será incluído em seu aplicativo final.
getActiveAndCompletedStats_noCompleted_returnsHundredZero()
para usar o assertThat
de Hamcrest em vez de assertEquals
.// TROQUE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
// COM
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
Observe que você pode usar import import org.hamcrest.Matchers.`is`
se solicitado.
O teste final será semelhante ao código abaixo.
StatisticsUtilsTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
// Create an active tasks (the false makes this active)
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// Call your function
val result = getActiveAndCompletedStats(tasks)
// Check the result
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
}
}
Este tutorial não lhe ensinará todos os prós e contras de Hamcrest, então se você quiser aprender mais confira o tutorial oficial.
Esta é uma tarefa opcional para prática.
Nesta tarefa, você escreverá mais testes usando JUnit e Hamcrest. Você também escreverá testes usando uma estratégia derivada da prática do programa de Desenvolvimento Guiado por Testes. O Desenvolvimento Guiado por Testes ou DGT é uma escola de pensamento de programação que diz que em vez de escrever seu código de recurso primeiro, você escreve seus testes primeiro. Em seguida, você escreve seu código de recurso com o objetivo de passar nos testes.
Escreva testes para quando você tiver uma lista de tarefas normal:
activeTasks
deve ser 0f
, e a porcentagem de tarefas concluídas deve ser 100f
.40f
e a porcentagem ativa deve ser 60f
.O código para getActiveAndCompletedStats
conforme escrito tem um erro. Observe como ele não trata corretamente com o que acontece se a lista estiver vazia ou nula. Em ambos os casos, ambas as porcentagens devem ser zero.
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
val activePercent = 100 * numberOfActiveTasks / totalTasks
val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks
return StatsResult(
activeTasksPercent = activePercent.toFloat(),
completedTasksPercent = completePercent.toFloat()
)
}
Para corrigir o código e escrever testes, você usará o desenvolvimento guiado a testes. O Desenvolvimento Guiado a Testes segue estas etapas.
Em vez de começar corrigindo o erro, você começará escrevendo os testes primeiro. Em seguida, você pode confirmar que tem testes que o protegem de reintroduzir acidentalmente esses erros no futuro.
emptyList()
), então ambas as porcentagens devem ser 0f.null
, e ambas as porcentagens devem ser 0f. Agora que você tem seus testes, corrija o erro.
getActiveAndCompletedStats
retornando 0f
se tasks
for null
ou vazio:internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
Seguindo o DGT e escrevendo os testes primeiro, você ajudou a garantir que:
Aqui estão todos os testes e o código de recurso correspondente.
StatisticsUtilsTest.kt
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
val tasks = listOf(
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed with an active task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 100 and 0
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
val tasks = listOf(
Task("title", "desc", isCompleted = true)
)
// When the list of tasks is computed with a completed task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 0 and 100
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(100f))
}
@Test
fun getActiveAndCompletedStats_both_returnsFortySixty() {
// Given 3 completed tasks and 2 active tasks
val tasks = listOf(
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = false),
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed
val result = getActiveAndCompletedStats(tasks)
// Then the result is 40-60
assertThat(result.activeTasksPercent, `is`(40f))
assertThat(result.completedTasksPercent, `is`(60f))
}
@Test
fun getActiveAndCompletedStats_error_returnsZeros() {
// When there's an error loading stats
val result = getActiveAndCompletedStats(null)
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_empty_returnsZeros() {
// When there are no tasks
val result = getActiveAndCompletedStats(emptyList())
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
}
StatisticsUtils.kt
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
Excelente trabalho com o básico de escrever e executar testes! A seguir, você aprenderá a escrever testes ViewModel
e LiveData
básicos.
No restante do tutorial, você aprenderá como escrever testes para duas classes Android que são comuns na maioria dos aplicativos—ViewModel
e LiveData
.
Você começa escrevendo testes para o TasksViewModel
.
Você vai se concentrar em testes que têm toda a sua lógica no modelo de vista e não dependem do código do repositório. O código do repositório envolve código assíncrono, bancos de dados e chamadas de rede, que adicionam complexidade ao teste. Você vai evitar isso por enquanto e se concentrar em escrever testes para a funcionalidade ViewModel que não testa nada diretamente no repositório.
O teste que você escreverá irá verificar se quando você chama o método addNewTask
, o Event
para abrir a nova janela de tarefa é disparado. Aqui está o código do aplicativo que você testará.
TasksViewModel.kt
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
Seguindo as mesmas etapas que você fez para StatisticsUtilTest
, nesta etapa, você cria um arquivo de teste para TasksViewModelTest
.
tasks
, TasksViewModel.
TasksViewModel
-> Generate -> Test.Nesta etapa, você adiciona um teste de modelo de vista para testar se, ao chamar o método addNewTask
, o Event
para abrir a nova janela de tarefa é disparado.
addNewTask_setsNewTaskEvent
.TasksViewModelTest.kt
class TasksViewModelTest {
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh TasksViewModel
// When adding a new task
// Then the new task event is triggered
}
}
E quanto ao contexto do aplicativo?
Quando você cria uma instância de TasksViewModel
para testar, seu construtor requer um Contexto de aplicativo. Mas, neste teste, você não está criando um aplicativo completo com atividades, IU e fragmentos, então como obter um contexto de aplicativo?
TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(???)
As Bibliotecas de teste do AndroidX incluem classes e métodos que fornecem versões de componentes como aplicativos e atividades destinados a testes. Quando você tem um teste local em que precisa de classes simuladas do Android framework (como um contexto de aplicativo), siga estas etapas para configurar o teste AndroidX de maneira adequada:
Você vai concluir essas etapas e then entender o que eles fazem juntos.
build.gradle
do módulo do aplicativo para adicionar o núcleo do AndroidX Test e as dependências ext, bem como a dependência de teste Robolectric.app/build.gradle
// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
@RunWith(AndroidJUnit4::class)
acima de sua classe de teste.TasksViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}
Neste ponto, você pode usar a biblioteca de teste??? AndroidX. Isso inclui o método ApplicationProvider.getApplicationContex
t
, que obtém um Contexto de aplicativo.
TasksViewModel
usando ApplicationProvider.getApplicationContext()
da biblioteca de teste AndroidX.TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
addNewTask
em tasksViewModel
.TasksViewModelTest.kt
tasksViewModel.addNewTask()
Neste ponto, seu teste deve ser semelhante ao código abaixo.
TasksViewModelTest.kt
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
// TODO test LiveData
}
AndroidX Test é uma coleção de bibliotecas para teste. Inclui classes e métodos que fornecem versões de componentes como Aplicativos e Atividades, destinados a testes. Como exemplo, este código que você escreveu é um exemplo de função de teste AndroidX para obter um contexto de aplicativo.
ApplicationProvider.getApplicationContext()
Um dos benefícios das APIs de teste do AndroidX é que elas são criadas para funcionar tanto para testes locais and instrumentados. Isso é bom porque:
Por exemplo, como você escreveu seu código usando bibliotecas de teste AndroidX, você pode mover sua classe TasksViewModelTest
da pasta test
para a pasta androidTest
e a os testes ainda serão executados. O getApplicationContext()
funciona de maneira um pouco diferente, dependendo se está sendo executado como um teste local ou instrumentado:
O ambiente Android simulado que o AndroidX Test usa para testes locais é fornecido por Robolectric. Robolectric é uma biblioteca que cria um ambiente Android simulado para testes e roda mais rápido do que inicializar um emulador ou rodar em um dispositivo. Sem a dependência Robolectric, você obterá este erro:
O que@RunWith(AndroidJUnit4::class)
faz?
Um executador de testeé um componente JUnit que executa testes. Sem um executor de teste, seus testes não seriam executados. Existe um executor de teste padrão fornecido pelo JUnit que você obtém automaticamente. @RunWith
troca esse executor de teste padrão.
O executor de teste AndroidJUnit4
permite que o AndroidX Test execute seu teste de forma diferente, dependendo se eles são instrumentados ou testes locais.
Ao executar o código, observe que o Robolectric é usado.
Por causa do AndroidX Test e do AndroidJunit4 test runner???, isso é feito sem que você escreva diretamente uma única linha de código Robolectric!
Você pode notar dois avisos.
No such manifest file: ./AndroidManifest.xml
"WARN: Android SDK 29 requires Java 9..."
Você pode corrigir o aviso No such manifest file: ./AndroidManifest.xml
, atualizando seu arquivo gradle.
app/build.gradle
// Always show the result of every unit test when running via command line, even if it passes.
testOptions.unitTests {
includeAndroidResources = true
// ...
}
O aviso "WARN: Android SDK 29 requires Java 9..."
é mais complicado. A execução de testes no Android Q requer Java 9. Em vez de tentar configurar o Android Studio para usar Java 9, para este tutorial, mantenha seu destino e compile o SDK em 28.
Em resumo:
test
porque seu código geralmente não requer Android.test
, poderá adicionar a dependência Robolectric e a anotação @RunWith(AndroidJUnit4::class)
.Parabéns, você está usando a biblioteca de teste do AndroidX e o Robolectric para executar um teste. Seu teste não terminou (você ainda não escreveu uma declaração assert, ela apenas diz // TODO test LiveData
). Você aprenderá a escrever declarações assert com LiveData
a seguir.
Nesta tarefa, você aprenderá como declarar corretamente o valor LiveData
.
Foi aqui que você parou sem o teste do modelo de vista addNewTask_setsNewTaskEvent
.
TasksViewModelTest.kt
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
// TODO test LiveData
}
Para testar o LiveData
, é recomendável fazer duas coisas:
InstantTaskExecutorRule
LiveData
InstantTaskExecutorRule
é uma JUnit Rule???. Quando você o usa com a anotação @get:Rule
, faz com que algum código na classe InstantTaskExecutorRule
seja executado antes e depois dos testes (para ver o código exato, você pode usar o atalho de teclado Command+B para ver o arquivo).
Esta regra executa todos os trabalhos de segundo plano relacionados aos componentes de arquitetura na mesma thread para que os resultados do teste ocorram de forma síncrona e em uma ordem repetível. Ao escrever testes que incluem teste LiveData, use esta regra!
app/build.gradle
testImplementation "androidx.arch.core: core-testing: $ archTestingVersion"
TasksViewModelTest.kt
InstantTaskExecutorRule
dentro da classe TasksViewModelTest
.TasksViewModelTest.kt
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Other code...
}
Sua próxima etapa é garantir que o LiveData
que você está testando seja observado.
Quando você usa LiveData
, normalmente tem uma atividade ou fragmento (LifecycleOwner
) observe o LiveData
.
viewModel.resultLiveData.observe(fragment, Observer {
// Observer code here
})
Essa observação é importante. Você precisa de observadores ativos no LiveData
para
onChanged
.Para obter o comportamento esperado do LiveData
para o LiveData
do seu modelo de vista, você precisa observar o LiveData
com um LifecycleOwner
.
Isso representa um problema: em seu teste TasksViewModel
, você não tem uma atividade ou fragmento para observar seu LiveData
. Para contornar isso, você pode usar o método observeForever
, que garante que o LiveData
seja constantemente observado, sem a necessidade de um LifecycleOwner
. Quando você observeForever
, você precisa se lembrar de remover seu observador ou arriscar um vazamento do observador.
Isso se parece com o código abaixo. Examine:
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// Create observer - no need for it to do anything!
val observer = Observer<Event<Unit>> {}
try {
// Observe the LiveData forever
tasksViewModel.newTaskEvent.observeForever(observer)
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.value
assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
} finally {
// Whatever happens, don't forget to remove the observer!
tasksViewModel.newTaskEvent.removeObserver(observer)
}
}
Isso é muito código clichê??? para observar um único LiveData
em um teste! Existem algumas maneiras de se livrar desse boilerplate. Você vai criar uma função de extensão chamada LiveDataTestUtil
para tornar a adição de observadores mais simples.
LiveDataTestUtil.kt
em seu conjunto de origem test
.
LiveDataTestUtil.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
Este é um método bastante complicado. Ele cria uma função de extensão Kotlin chamada getOrAwaitValue
que adiciona um observador, obtém o valor de LiveData
e, em seguida, limpa o observador - basicamente um curto, versão reutilizável do código observeForever
mostrado acima. Para obter uma explicação completa desta classe, verifique esta postagem do blog.
Nesta etapa, você usa o método getOrAwaitValue
e escreve uma instrução assert que verifica se o newTaskEvent
foi acionado.
LiveData
para newTaskEvent
usando getOrAwaitValue
.val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled(), (not(nullValue())))
O teste completo deve ser semelhante ao código abaixo.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled(), not(nullValue()))
}
}
Agora que você viu como escrever um teste, escreva um sozinho. Nesta etapa, usando as habilidades que você aprendeu, pratique escrever outro teste TasksViewModel
.
Você escreverá setFilterAllTasks_tasksAddViewVisible()
. Este teste deve verificar se você configurou o tipo de filtro para mostrar todas as tarefas, se o botão Add task está visível.
addNewTask_setsNewTaskEvent()
para referência, escreva um teste em TasksViewModelTest
chamado setFilterAllTasks_tasksAddViewVisible()
que define o modo de filtragem para ALL_TASKS
e afirma que o tasksAddViewVisible
LiveData é true
.
Use o código abaixo para começar.
TasksViewModelTest
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
// When the filter type is ALL_TASKS
// Then the "Add task" action is visible
}
Nota:
TasksFilterType
enum para todas as tarefas é ALL_TASKS.
LiveData
tasksAddViewVisible.
Compare sua solução com a solução abaixo.
TasksViewModelTest
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
}
Verifique se você faz o seguinte:
tasksViewModel
usando a mesma instrução AndroidX ApplicationProvider.getApplicationContext()
.setFiltering
, passando o enum do tipo de filtro ALL_TASKS
.tasksAddViewVisible
é verdadeiro, usando o método getOrAwaitNextValue
.Observe como, no início de ambos os testes, você define um TasksViewModel
.
TasksViewModelTest
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
Ao repetir o código de configuração para vários testes, você pode usar a anotação @Before para criar um método de configuração e remover o código repetido. Como todos esses testes vão testar o TasksViewModel
e precisam de um modelo de vista, mova este código para um bloco @Before
.
lateinit
chamada tasksViewModel|
.setupViewModel
.@Before
.setupViewModel
.TasksViewModelTest
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
Aviso
Não faça o seguinte, não inicialize o
tasksViewModel
com sua definição:
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
Isso fará com que a mesma instância seja usada para todos os testes. Isso é algo que você deve evitar porque cada teste deve ter uma nova instância do assunto em teste (o ViewModel neste caso).
Seu código final para TasksViewModelTest
deve ser semelhante ao código abaixo.
TasksViewModelTest
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
@Test
fun addNewTask_setsNewTaskEvent() {
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.awaitNextValue()
assertThat(
value?.getContentIfNotHandled(), (not(nullValue()))
)
}
@Test
fun getTasksAddViewVisible() {
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
}
}
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_1
Alternativamente, você pode baixar o repositório como um arquivo Zip, descompactá-lo e abri-lo no Android Studio.
Este tutorial cobriu:
test
) e testes de instrumentação (androidTest
). Amostras:
Documentação do desenvolvedor Android:
Vídeos:
De outros:
Comece a próxima lição:
Comece a próxima lição:
Para obter links para outros tutoriais neste curso, consulte a página inicial de tutoriais do Android avançado em Kotlin.