Testes Android com Kotlin deixam de ser um assunto opcional quando o app começa a ter login, estado offline, sincronização em segundo plano, pagamento, feature flags ou telas em Jetpack Compose. Sem uma estratégia mínima, cada refatoração de ViewModel vira medo, cada mudança de layout pode quebrar um fluxo importante e cada bug de sincronização volta em produção depois de parecer resolvido no emulador.

O objetivo deste guia é montar uma estratégia prática para 2026, sem transformar testes em um projeto paralelo. A ideia é combinar testes unitários rápidos para regras e estado, testes de integração para persistência e sincronização, testes de UI em Compose quando a tela é o contrato, e poucos testes end-to-end com Maestro para jornadas críticas.

Se você ainda está construindo a base do app, leia também o roadmap de desenvolvedor Android, o guia de testes em Kotlin e a trilha de Android offline-first com Kotlin. Eles ajudam a decidir o que deve estar em ViewModel, repository, Room, WorkManager e UI.

A pirâmide de testes para Android Kotlin

Um app Android moderno costuma misturar três tipos de código: lógica pura em Kotlin, integração com bibliotecas Android e interface. Cada tipo pede um teste diferente.

  • Testes unitários JVM: validam use cases, validators, mappers, reducers, ViewModels, repositories fake e regras de domínio.
  • Testes de coroutines e Flow: verificam estados assíncronos, cancelamento, retry, debounce, sincronização e emissão de eventos.
  • Testes locais com Robolectric: ajudam quando uma parte usa recursos do Android framework, mas ainda pode rodar fora do emulador.
  • Testes instrumentados: rodam em device ou emulador e validam Room real, Compose UI, permissões, navegação e componentes Android.
  • Testes end-to-end: cobrem jornadas de usuário com Maestro, Espresso ou outra ferramenta de automação.

A regra prática é simples: deixe a maior parte do comportamento fora da UI e teste na JVM. Use emulador apenas quando o comportamento realmente depende do Android. Isso mantém a suíte rápida e reduz flakiness.

Dependências base no Gradle

Em um projeto Android com Gradle Kotlin DSL, uma base comum fica parecida com esta:

dependencies {
    testImplementation("junit:junit:4.13.2")
    testImplementation("io.mockk:mockk:1.13.12")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
    testImplementation("app.cash.turbine:turbine:1.1.0")

    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
}

Use versões compatíveis com seu Android Gradle Plugin, Kotlin e Compose BOM. O importante é separar o que roda em test do que roda em androidTest. Misturar tudo no emulador deixa o feedback lento demais para pull request.

Testando ViewModel com StateFlow

ViewModel costuma ser o melhor ponto para começar. Ela coordena estado de tela, chama casos de uso, transforma erro em mensagem e decide quando mostrar loading. Se esse código estiver testado, boa parte da UI vira uma função simples do estado.

@Test
fun `deve emitir sucesso ao carregar tarefas`() = runTest {
    val repository = mockk<TarefaRepository>()
    coEvery { repository.listar() } returns listOf(Tarefa("Estudar Compose"))

    val viewModel = TarefasViewModel(repository)

    viewModel.estado.test {
        assertEquals(TarefasState.Loading, awaitItem())

        viewModel.carregar()

        assertEquals(TarefasState.Sucesso(listOf("Estudar Compose")), awaitItem())

        cancelAndIgnoreRemainingEvents()
    }
}

Para esse tipo de teste, evite delay real e Thread.sleep. Use runTest, dispatchers controlados e Turbine para ler emissões de Flow ou StateFlow. Isso deixa o teste determinístico e rápido.

Testando Compose sem acoplar ao texto

Em Compose, prefira testar contratos estáveis. Texto visível muda com copy, tradução e experimento. Tags semânticas costumam ser mais resistentes:

@Composable
fun LoginScreen(state: LoginState, onEntrar: () -> Unit) {
    Column {
        TextField(
            value = state.email,
            onValueChange = {},
            modifier = Modifier.testTag("login_email"),
        )
        Button(
            onClick = onEntrar,
            modifier = Modifier.testTag("login_submit"),
        ) {
            Text("Entrar")
        }
    }
}

O teste instrumentado fica direto:

@Test
fun deveEnviarLoginAoTocarNoBotao() {
    var enviado = false

    composeRule.setContent {
        LoginScreen(LoginState(email = "[email protected]"), onEntrar = { enviado = true })
    }

    composeRule.onNodeWithTag("login_submit").performClick()

    assertTrue(enviado)
}

Não teste cada detalhe visual com teste funcional. Para regressão visual, avalie screenshot testing. Para regra de negócio, volte para ViewModel ou use case. Teste de UI deve provar interação, acessibilidade básica, estados vazios, erro e navegação crítica.

Room, DataStore e WorkManager

Persistência e trabalho em segundo plano merecem cuidado porque bugs aparecem depois de uso real: app fechado, rede instável, conflito de dados, retry duplicado e migração de schema.

Para Room, teste DAOs e migrations com banco instrumentado quando o comportamento depende de SQLite real. Para DataStore, isole diretórios temporários e valide as emissões com Flow. Para WorkManager, mantenha a regra pesada em um use case testável e deixe o worker como coordenador fino.

Um bom recorte é:

  • teste unitário para decidir quais registros precisam sincronizar;
  • teste de repository para garantir que o estado local muda corretamente;
  • teste instrumentado curto para provar que o worker agenda e chama o caso de uso;
  • teste E2E apenas para uma jornada offline-first importante.

Isso evita um worker gigante e difícil de testar.

Quando usar Espresso e quando usar Maestro

Espresso ainda funciona bem para interações Android nativas e integrações dentro do próprio app. Em Compose, a API compose-ui-test costuma ser mais natural. Maestro entra melhor quando você quer validar uma jornada de produto como uma pessoa usuária faria: abrir app, fazer login, navegar, criar item, fechar, reabrir e confirmar persistência.

Um fluxo Maestro simples pode ser:

appId: br.dev.kotlin.exemplo
---
- launchApp
- tapOn: "Entrar"
- inputText: "[email protected]"
- tapOn: "Continuar"
- assertVisible: "Minhas tarefas"

Use Maestro para poucos fluxos de alto valor. Se você automatizar vinte jornadas longas, a manutenção cresce rápido. Comece com login, onboarding, criação de item principal, fluxo offline e deep link crítico.

CI/CD sem deixar a suíte lenta

Uma pipeline saudável separa feedback rápido de validação pesada:

  1. ./gradlew testDebugUnitTest em todo pull request.
  2. ./gradlew ktlintCheck detekt junto dos testes unitários.
  3. ./gradlew connectedDebugAndroidTest em PRs que mexem em UI, Room, navegação ou integração Android.
  4. Maestro em branch principal, release candidate ou PRs de fluxos críticos.
  5. Relatórios de teste e screenshots como artefatos do CI para facilitar debug.

O guia de CI/CD para Kotlin mostra como organizar a automação. O ponto mais importante é não mascarar falhas essenciais com continue-on-error. Teste instável precisa ser corrigido, reduzido ou removido. Se ele falha aleatoriamente e ninguém investiga, ele deixa de proteger o produto.

Checklist para uma suíte Android sustentável

  • ViewModels e use cases rodam na JVM com runTest.
  • Estados de tela são modelados de forma explícita, com loading, sucesso, vazio e erro.
  • Composables importantes usam testTag ou semântica acessível estável.
  • Room, DataStore e WorkManager têm ao menos testes dos caminhos críticos.
  • Fluxos E2E cobrem poucas jornadas de alto impacto, não todos os botões.
  • CI separa teste rápido de teste instrumentado e mantém logs legíveis.
  • Bugs corrigidos viram teste quando o risco de regressão é real.

Próximos passos

Se seu app Android ainda não tem testes, comece pequeno. Escolha uma ViewModel que muda com frequência, escreva testes para sucesso e erro, depois adicione um teste de Compose para a tela mais crítica. Em seguida, cubra persistência local com Room ou DataStore e coloque a suíte básica no CI.

Com o tempo, você pode evoluir para screenshot testing, device farms, testes de acessibilidade e automações Maestro mais completas. Mas a base continua a mesma: regra fora da UI, estado observável, testes rápidos primeiro e emulador apenas onde ele realmente compra confiança.

Para avançar na trilha Android, combine este guia com MVVM em Kotlin, Room Database, DataStore Preferences, WorkManager e Android offline-first. Essa sequência mostra ao mercado que você sabe entregar app Kotlin com qualidade, não apenas montar telas.