Resposta rápida: use Espresso para validar interações dentro do seu app Android quando o teste precisa de uma Activity real, recursos, navegação, Views ou integração com componentes do framework. Escreva poucos cenários críticos, selecione elementos por IDs ou semântica estável, substitua rede e relógio por dependências controláveis e sincronize trabalho assíncrono com APIs observáveis ou IdlingResource — nunca com Thread.sleep. Em projetos 100% Jetpack Compose, prefira compose-ui-test para a árvore Compose; mantenha Espresso para telas legadas, fluxos híbridos, Intents e integrações Android que ele resolve melhor.

Espresso continua relevante mesmo com a adoção de Jetpack Compose. Muitos aplicativos brasileiros têm telas XML e Compose convivendo, SDKs de terceiros baseados em Views, WebView, permissões, Activities externas e jornadas que atravessam diferentes gerações da interface. O problema não é a ferramenta: é tentar transformar todo comportamento do produto em um teste instrumentado longo e frágil.

Este guia mostra como configurar Espresso com Kotlin, escrever testes legíveis, lidar com RecyclerView e Intents, sincronizar operações assíncronas, testar projetos híbridos e reduzir flakiness no CI. Para a estratégia completa — unitário, integração, Compose e end-to-end — leia também o guia de testes Android com Kotlin, Compose e Maestro e o guia geral de testes em Kotlin.

Quando usar Espresso — e quando não usar

Espresso roda em um dispositivo ou emulador e interage com a interface do aplicativo sob teste. Ele é uma boa escolha quando você precisa provar que:

  • uma View está visível, habilitada ou contém determinado texto;
  • tocar em um botão altera a tela ou abre outra Activity;
  • um formulário exibe erros de validação no lugar certo;
  • uma lista mostra itens retornados pelo repository;
  • uma Intent foi disparada com action e extras corretos;
  • uma tela híbrida XML + Compose continua navegável;
  • uma integração com recursos Android funciona no ambiente instrumentado.

Não use Espresso como primeira opção para regras de CPF, cálculo, ordenação, mapeamento de DTO, reducer ou estado de ViewModel. Esses casos devem rodar na JVM em milissegundos, com JUnit, MockK e kotlinx-coroutines-test. O artigo sobre JUnit 5 e MockK em Kotlin cobre essa camada.

Uma divisão prática é:

ProblemaFerramenta preferida
Regra de negócio e ViewModelteste unitário JVM
DAO e migration Roomteste instrumentado de integração
UI baseada em ViewsEspresso
UI 100% Composecompose-ui-test
Tela híbrida Views + ComposeEspresso + Compose Test
Jornada longa entre apps/sistemaMaestro ou UI Automator
Regressão visualscreenshot testing

Espresso não substitui testes de screenshot no Compose. Um verifica comportamento e estado acessível; o outro compara a aparência renderizada.

Configuração no Gradle Kotlin DSL

No módulo Android, mantenha as dependências instrumentadas em androidTestImplementation:

android {
    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    testOptions {
        animationsDisabled = true
    }
}

dependencies {
    androidTestImplementation("androidx.test.ext:junit:<versao-compativel>")
    androidTestImplementation("androidx.test:runner:<versao-compativel>")
    androidTestImplementation("androidx.test:rules:<versao-compativel>")
    androidTestImplementation("androidx.test.espresso:espresso-core:<versao-compativel>")
    androidTestImplementation("androidx.test.espresso:espresso-intents:<versao-compativel>")
    androidTestImplementation("androidx.test.espresso:espresso-contrib:<versao-compativel>")
}

Use o Version Catalog do Gradle para centralizar versões compatíveis com seu Android Gradle Plugin. Não copie números antigos de um tutorial sem conferir a release estável adotada pelo projeto.

Um teste com ActivityScenarioRule pode começar assim:

@RunWith(AndroidJUnit4::class)
class LoginActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun loginInvalido_mostraMensagemDeErro() {
        onView(withId(R.id.email_input))
            .perform(typeText("email-invalido"), closeSoftKeyboard())

        onView(withId(R.id.login_button))
            .perform(click())

        onView(withId(R.id.email_error))
            .check(matches(withText("Digite um e-mail válido")))
            .check(matches(isDisplayed()))
    }
}

A estrutura central do Espresso tem três partes:

  1. onView(...) encontra o elemento com um matcher;
  2. perform(...) executa uma ação;
  3. check(matches(...)) valida uma asserção.

Essa separação deixa a intenção clara e permite combinar matchers sem criar seletores baseados em posição de pixel.

Matchers estáveis: selecione pelo contrato

O seletor mais simples costuma ser withId. IDs são estáveis, rápidos e independem de tradução:

onView(withId(R.id.save_button))
    .check(matches(allOf(isDisplayed(), isEnabled())))

Texto também pode ser útil quando o texto é o próprio comportamento público:

onView(withText(R.string.order_confirmed))
    .check(matches(isDisplayed()))

Evite localizar um botão apenas por “o terceiro filho do segundo LinearLayout”. Essa árvore muda durante refatorações visuais sem que o contrato do produto tenha mudado.

Quando há elementos repetidos, combine matchers:

onView(
    allOf(
        withId(R.id.price),
        isDescendantOfA(withId(R.id.cart_summary)),
    ),
).check(matches(withText("R$ 149,90")))

Em acessibilidade e testes, o mesmo princípio ajuda: componentes com IDs, labels e conteúdo semântico claro são mais fáceis de usar e automatizar. Veja o guia de acessibilidade no Android com Compose e Kotlin para tratar semântica como parte da interface pública.

Page Objects sem esconder o comportamento

Repetir seletores em dezenas de testes dificulta manutenção. Um Screen Object pequeno pode agrupar ações e asserções:

class LoginScreen {
    fun preencherEmail(email: String) = apply {
        onView(withId(R.id.email_input))
            .perform(replaceText(email), closeSoftKeyboard())
    }

    fun tocarEmEntrar() = apply {
        onView(withId(R.id.login_button)).perform(click())
    }

    fun validarErro(mensagem: String) = apply {
        onView(withId(R.id.email_error))
            .check(matches(withText(mensagem)))
            .check(matches(isDisplayed()))
    }
}

O teste fica legível:

@Test
fun emailInvalido_naoAvanca() {
    LoginScreen()
        .preencherEmail("invalido")
        .tocarEmEntrar()
        .validarErro("Digite um e-mail válido")
}

Não transforme o Screen Object em um framework próprio com lógica condicional, sleeps e estado global. Ele deve apenas dar nomes de negócio a interações repetidas. A regra testada continua no app.

Testando RecyclerView

Para listas baseadas em RecyclerView, espresso-contrib oferece ações específicas:

onView(withId(R.id.products_list))
    .perform(
        RecyclerViewActions.scrollTo<ProdutoViewHolder>(
            hasDescendant(withText("Café especial")),
        ),
    )

onView(withText("Café especial"))
    .check(matches(isDisplayed()))

Para clicar em uma ação dentro do item, prefira um matcher que descreva o conteúdo, e não uma posição fixa. A posição 4 pode virar posição 5 depois de ordenação, paginação ou feature flag.

Se o conteúdo vem da rede, injete um fake repository no build de teste ou use um servidor HTTP controlado. Um teste que depende da API pública, do Wi-Fi e de dados mutáveis é um teste de produção disfarçado — lento, imprevisível e difícil de diagnosticar.

Espresso Intents: valide navegação externa sem abrir outro app

espresso-intents permite interceptar e verificar Intents. Isso é útil para câmera, navegador, compartilhamento, discador e seletor de arquivos.

@RunWith(AndroidJUnit4::class)
class AjudaActivityTest {

    @get:Rule
    val intentsRule = IntentsTestRule(AjudaActivity::class.java)

    @Test
    fun tocarEmDocumentacao_abreUrlCorreta() {
        onView(withId(R.id.documentation_button)).perform(click())

        intended(
            allOf(
                hasAction(Intent.ACTION_VIEW),
                hasData("https://kotlin.dev.br/guias/guia-kotlin-android-desenvolvimento/"),
            ),
        )
    }
}

Quando a Activity externa devolver um resultado, use intending(...) para responder com um ActivityResult fake. Assim o teste valida seu contrato sem depender de outro aplicativo instalado no emulador.

Para links que devem abrir diretamente uma tela do seu app, complemente com o guia de App Links e Deep Links no Android.

Sincronização: por que Thread.sleep é um erro

Espresso sincroniza automaticamente com a main thread e com tarefas conhecidas pelo framework. Porém, ele não adivinha todo trabalho assíncrono de rede, executors próprios, callbacks de SDK ou polling.

Este código é frágil:

onView(withId(R.id.refresh_button)).perform(click())
Thread.sleep(2_000)
onView(withText("Sincronizado")).check(matches(isDisplayed()))

Se a máquina estiver rápida, o teste perde dois segundos. Se estiver lenta, ele falha. A solução preferida é tornar o estado observável e substituir a dependência externa por fake. Quando isso não for possível, conecte a operação a um CountingIdlingResource:

object AppIdlingResource {
    val resource = CountingIdlingResource("app_background_work")
}

class PedidoRepository(
    private val api: PedidoApi,
) {
    suspend fun atualizar(): List<Pedido> {
        AppIdlingResource.resource.increment()
        return try {
            api.buscarPedidos()
        } finally {
            AppIdlingResource.resource.decrement()
        }
    }
}

No teste:

@Before
fun registrarIdlingResource() {
    IdlingRegistry.getInstance().register(AppIdlingResource.resource)
}

@After
fun removerIdlingResource() {
    IdlingRegistry.getInstance().unregister(AppIdlingResource.resource)
}

Mantenha incremento e decremento balanceados, inclusive em exceções. Um contador que nunca volta a zero faz a suíte travar. Também evite espalhar código de teste pelo domínio: encapsule a integração e, se possível, exponha uma implementação no build debug/test.

Espresso e Jetpack Compose no mesmo projeto

Para uma tela Compose, use a API de teste do Compose:

@get:Rule
val composeRule = createAndroidComposeRule<MainActivity>()

@Test
fun salvarPerfil_mostraConfirmacao() {
    composeRule.onNodeWithTag("profile_name")
        .performTextInput("Ana")

    composeRule.onNodeWithTag("profile_save")
        .performClick()

    composeRule.onNodeWithText("Perfil salvo")
        .assertIsDisplayed()
}

Em telas híbridas, você pode usar Espresso para a View tradicional e composeRule para o conteúdo Compose no mesmo processo. Só não tente buscar um Text Compose com onView(withText(...)): Compose mantém sua própria árvore semântica.

A escolha fica assim:

  • onView para Views Android;
  • onNode para nós Compose;
  • Espresso Intents para Intents;
  • UI Automator para elementos fora do processo do app;
  • Maestro para jornadas E2E maiores e legíveis em YAML.

Se você está alinhando dependências de UI, consulte o guia do Compose BOM e o guia completo de Jetpack Compose.

Como reduzir flakiness em testes instrumentados

Flakiness não é “normal do Android”; quase sempre existe uma causa concreta. Use este checklist:

  1. Desative animações no ambiente de teste, sem depender do timing visual.
  2. Controle rede e banco com fakes, fixtures ou estado limpo por teste.
  3. Não compartilhe ordem: cada teste deve passar sozinho e em qualquer sequência.
  4. Feche teclado quando ele pode cobrir botões ou alterar scroll.
  5. Use matchers específicos: múltiplas Views com o mesmo texto geram ambiguidade.
  6. Espere por estado, não por tempo: Idling Resources, semântica ou dependências síncronas controladas.
  7. Evite posição fixa em listas quando o conteúdo pode mudar.
  8. Capture artefatos no CI: logcat, vídeo, screenshot e relatório ajudam a separar bug de infraestrutura.
  9. Teste uma responsabilidade por cenário: fluxos gigantes falham longe da causa.
  10. Rode em API levels representativos, mas não multiplique a matriz sem critério.

Uma boa suíte instrumentada não precisa ter centenas de casos. Dez cenários críticos, determinísticos e rápidos protegem mais que oitenta testes ignorados pelo time.

Organização no CI

Separe os testes por custo:

# Feedback rápido em todo pull request
./gradlew testDebugUnitTest

# Instrumentados em PRs de UI ou integração Android
./gradlew connectedDebugAndroidTest

Em CI, use emulador com imagem e API level fixados, cacheie dependências Gradle e publique relatórios mesmo quando o job falhar. Sharding pode ajudar suites grandes, mas primeiro elimine cenários redundantes e sleeps.

O guia de GitHub Actions para Kotlin mostra a base de automação. Para performance, mantenha Macrobenchmark em uma tarefa separada: benchmark não deve competir com asserções funcionais comuns. Veja Baseline Profiles e Macrobenchmark para essa trilha.

Exemplo de estratégia para um fluxo de login

Em vez de testar tudo com Espresso, distribua o risco:

  • unitário: validator de e-mail e senha;
  • unitário: ViewModel emite loading, sucesso e erro;
  • integração: repository interpreta respostas HTTP;
  • Espresso/Compose: campos aceitam entrada, erro aparece e sucesso navega;
  • Maestro: uma única jornada de login até a home em ambiente controlado;
  • screenshot: estados visualmente importantes, como erro e loading.

Essa divisão torna a falha diagnóstica. Se o validator quebra, você não precisa iniciar emulador. Se a Activity não abre a home, o teste de UI aponta a integração. Se o backend de homologação está fora, somente o E2E correspondente deve sentir o problema.

Erros comuns com Espresso

Colocar lógica de negócio no teste de UI

Se o teste precisa cadastrar dez objetos pela interface antes de chegar ao cenário, crie fixture via repository fake ou API de setup. A UI não precisa ser o caminho para preparar todos os dados.

Usar texto traduzido como único seletor

Texto é contrato quando a copy importa, mas IDs e semântica são melhores para ações. Caso contrário, uma tradução quebra o teste sem quebrar a funcionalidade.

Tentar testar o sistema operacional inteiro

Espresso trabalha dentro do processo do app. Diálogos de permissão, notificações e telas de outro app podem exigir UI Automator ou uma camada abstraída. Teste sua reação ao resultado, não cada pixel do diálogo do Android.

Adicionar retry automático para esconder falha

Retry pode amortecer infraestrutura instável, mas não corrige corrida, estado vazando ou sincronização errada. Registre a causa antes de aceitar uma nova tentativa como solução.

Checklist para adotar Espresso hoje

  • regras de negócio já estão cobertas na JVM;
  • o runner instrumentado está configurado;
  • dependências ficam em androidTestImplementation;
  • Views importantes têm IDs e labels estáveis;
  • rede, banco, relógio e feature flags podem ser controlados;
  • nenhum teste usa Thread.sleep;
  • Intents externas são interceptadas quando possível;
  • Compose usa onNode, Views usam onView;
  • testes não dependem da ordem de execução;
  • CI publica relatório, logcat e screenshots de falha;
  • a suíte cobre jornadas críticas, não cada botão.

Conclusão

Espresso ainda é uma ferramenta importante para Android com Kotlin, especialmente em bases com Views, telas híbridas, RecyclerView, Intents e integração profunda com o framework. O caminho para testes estáveis não é escrever mais automação: é escolher a camada certa, controlar dependências, sincronizar por estado e manter cada cenário curto.

Comece por um fluxo que já causou regressão. Teste a regra na JVM, deixe apenas a interação indispensável no Espresso e coloque o cenário no CI. Depois amplie para navegação, listas e integrações de maior risco. Com essa disciplina, o teste instrumentado deixa de ser uma etapa lenta que “às vezes fica vermelho” e passa a ser uma proteção confiável para releases Android.

Para continuar, leia testes Android com Compose e Maestro, testes em Kotlin para JVM, Android e backend e testes de screenshot no Compose. Juntos, esses conteúdos formam uma estratégia de qualidade completa, do domínio à interface.