Capítulo 16: Testes Unitários, Testes de UI no Compose e Testes de Coroutines
Capítulo 16: Testes Unitários, Testes de UI no Compose e Testes de Coroutines
Especialização em Mobile Nativo com Android • Kotlin 2.0 & Jetpack Compose
🗺️ Mapa Conceitual da Pirâmide de Testes no Android Moderno
flowchart TD
subgraph PYRAMID["Pirâmide de Testes de Software Mobile"]
E2E["1. Testes de Integração / E2E (Dispositivo Real / Emulador com Espresso)"]
UI["2. Testes de Componentes Compose (ComposeTestRule & Semantics Tree)"]
UNIT["3. Testes Unitários de Domínio e ViewModels (JVM Host Puro / JUnit 5 + MockK + Turbine)"]
E2E --> UI --> UNIT
end
subgraph UNIT_TOOLS["Ferramentas de Teste Unitário (Rápidos: < 200ms)"]
RUN_TEST["kotlinx.coroutines.test.runTest"]
TURBINE["Turbine: app.cash.turbine.test { awaitItem() }"]
MOCKK["MockK: coEvery / coVerify"]
RUN_TEST --> TURBINE
MOCKK --> RUN_TEST
end
subgraph COMPOSE_TOOLS["Ferramentas de Teste de UI no Compose"]
RULE["createComposeRule()"]
SEMANTICS["Busca Semântica: onNodeWithTag / onNodeWithText"]
ACTIONS["Ações & Asserções: performClick() / assertIsDisplayed()"]
RULE --> SEMANTICS --> ACTIONS
end
UNIT -.-> UNIT_TOOLS
UI -.-> COMPOSE_TOOLS
style UNIT fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style UI fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style E2E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style TURBINE fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style SEMANTICS fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
🏛️ 1. A Estratégia de Qualidade e Pirâmide de Testes no Android
Garantir a estabilidade de um aplicativo com dezenas de telas e fluxos assíncronos requer uma estratégia de testes automatizados dividida em camadas complementares:
- Testes Unitários Locais na JVM (70% do Esforço): Executam diretamente no computador de desenvolvimento ou servidor de CI/CD sem necessidade de emuladores Android. Testam entidades de domínio, regras de negócio de ViewModels e conversores de dados. Com
kotlinx-coroutines-teste a biblioteca Turbine, os tempos virtuais de coroutines são adiantados instantaneamente sem esperas arbitrárias deThread.sleep(). - Testes de UI Declarativa com Compose Test Rule (20% do Esforço): O Jetpack Compose não interage com a tela inspecionando pixels crus; ele gera uma Árvore Semântica (Semantics Tree) paralela à árvore de nós visuais. A biblioteca de testes oficial utiliza essa semântica para localizar nós por texto legível (
onNodeWithText) ou identificadores de automação (testTag), simulando gestos de clique e validando se o estado visual foi atualizado. - Determinismo Temporal com
runTest: O executorrunTestsubstitui os despachadores reais de Coroutine por umTestCoroutineScheduler. Qualquer chamada comdelay(10_000)é executada virtualmente em 0 milissegundos no teste, garantindo execuções de testes ultrarrápidas e determinísticas.
📊 2. Comparativo: Testes Unitários de JVM vs Testes Instrumentados no Dispositivo
| Característica | Testes Unitários na JVM (test/) |
Testes Instrumentados Android (androidTest/) |
|---|---|---|
| Ambiente de Execução | JVM da máquina hospedeira local (Host PC / CI) | Emulador Android real ou dispositivo físico conectado |
| Tempo de Inicialização | Instantâneo (~1 a 3 segundos para centenas de testes) | Lento (~30 a 60 segundos para subir o APK de teste) |
| Alvos de Teste | ViewModels, UseCases, Repositories, Redutores MVI | Banco Room real, Activities, Serviços, Gestos complexos de UI |
| Dependência do SDK Android | Nenhuma (ou simulado via mocks leves) | Acesso total ao android.content.Context real do SO |
| Custo de Infraestrutura CI | Baixo (roda em runners Linux leves e baratos) | Elevado (exige runners com virtualização de hardware KVM) |
💻 3. Laboratório Prático: Testando ViewModel com Turbine e MockK
O código abaixo demonstra um teste unitário profissional validando as emissões reativas de um ViewModel sem instanciar emuladores:
package br.com.portal.mobile.android.cap16
import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class NoticiasViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private val mockRepository = mockk<NoticiaRepository>()
private lateinit var viewModel: NoticiasViewModel
@Before
fun configurar() {
// Redireciona o Dispatchers.Main do Android para o despachador virtual de testes
Dispatchers.setMain(testDispatcher)
}
@After
fun finalizar() {
Dispatchers.resetMain()
}
@Test
fun `quando carregarNoticias tiver sucesso, deve emitir estado com lista preenchida`() = runTest {
// 1. Arrange: Configuração dos mocks
val noticiasMock = listOf("Lançamento Android 15", "Kotlin 2.0 Estável")
coEvery { mockRepository.obterNoticias() } returns flowOf(noticiasMock)
viewModel = NoticiasViewModel(mockRepository)
// 2. Act & Assert com a biblioteca Turbine
viewModel.uiState.test {
// Primeiro item emitido é o estado inicial de Loading
val estadoInicial = awaitItem()
assertEquals(true, estadoInicial.carregando)
// Avança o tempo virtual do despachador
testDispatcher.scheduler.advanceUntilIdle()
// Segundo item emitido após o sucesso da coroutine
val estadoSucesso = awaitItem()
assertFalse(estadoSucesso.carregando)
assertEquals(2, estadoSucesso.itens.size)
assertEquals("Lançamento Android 15", estadoSucesso.itens[0])
cancelAndIgnoreRemainingEvents()
}
}
}
⚠️ 4. Armadilhas Comuns na Criação de Testes Mobile
- Uso de
Thread.sleep()em Testes de UI: Adicionar pausas estáticasThread.sleep(2000)para esperar chamadas de rede terminarem torna a suíte de testes lenta, intermitente e propensa a falsos negativos (Flaky Tests). Prefira semprewaitUntilDoesNotExistou APIs de sincronização do Compose Test Rule. - Esquecer de Resetar o
Dispatchers.Main: Ao substituir o despachador principal em testes unitários comDispatchers.setMain(), sempre invoqueDispatchers.resetMain()no método com@After. Caso contrário, os testes seguintes podem falhar com vazamento de estado de dispatcher. - Acoplamento a Strings Visuais nos Testes de UI: Buscar nós estritamente por texto visível (
onNodeWithText("Comprar")) pode quebrar todos os testes ao traduzir o app para outros idiomas (i18n). UtilizeModifier.testTag("btn_comprar")para seletores estáveis de automação.