Sincronizar seus testes

Por padrão, os testes do Compose são sincronizados com sua IU. Quando você chama uma declaração ou uma ação com a ComposeTestRule, o teste é sincronizado antecipadamente enquanto aguarda até que a árvore da IU fique inativa.

Normalmente, não é necessário fazer nada. No entanto, existem alguns casos extremos que você precisa conhecer.

Quando um teste é sincronizado, o tempo do app Compose é avançado usando um relógio virtual. Isso significa que os testes do Compose não são executados em tempo real, para que possam ser realizados o mais rápido possível.

No entanto, caso você não use os métodos que sincronizam os testes, nenhuma recomposição vai ocorrer, e a IU aparentará estar pausada.

@Test
fun counterTest() {
    val myCounter = mutableStateOf(0) // State that can cause recompositions.
    var lastSeenValue = 0 // Used to track recompositions.
    composeTestRule.setContent {
        Text(myCounter.value.toString())
        lastSeenValue = myCounter.value
    }
    myCounter.value = 1 // The state changes, but there is no recomposition.

    // Fails because nothing triggered a recomposition.
    assertTrue(lastSeenValue == 1)

    // Passes because the assertion triggers recomposition.
    composeTestRule.onNodeWithText("1").assertExists()
}

Esse requisito se aplica apenas a hierarquias do Compose, e não ao restante do app.

Como desativar a sincronização automática

Quando você chama uma declaração ou ação usando ComposeTestRule, como assertExists(), seu teste é sincronizado com a IU do Compose. Em alguns casos, pode ser necessário interromper essa sincronização e controlar o relógio. Por exemplo, você pode controlar o tempo para fazer capturas de tela precisas de uma animação em um ponto em que a IU ainda estaria ocupada. Para desativar a sincronização automática, defina a propriedade autoAdvance em mainClock como false:

composeTestRule.mainClock.autoAdvance = false

Normalmente, isso fará com que o tempo seja avançado. É possível avançar exatamente um frame com advanceTimeByFrame() ou um intervalo específico com advanceTimeBy():

composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)

Recursos inativos

O Compose pode sincronizar testes e a IU para que todas as ações e declarações sejam executadas em estado inativo enquanto estão aguardando ou avançando o relógio conforme necessário. No entanto, algumas operações assíncronas com resultados que afetam o estado da IU podem ser executadas em segundo plano enquanto não afetam os testes.

Crie e registre esses recursos de inatividade no teste, para que eles sejam considerados ao decidir se o app sendo testado está ocupado ou inativo. Não é necessário fazer nada, a menos que você precise registrar outros recursos de inatividade, por exemplo, executar um job em segundo plano que não esteja sincronizado com o Espresso ou Compose.

Essa API é muito semelhante aos Recursos de inatividade do Espresso, usados para indicar se o assunto sendo testado está inativo ou ocupado. Use a regra de teste do Compose para registrar a implementação da IdlingResource.

composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)

Sincronização manual

Em alguns casos, você precisa sincronizar a IU do Compose com outras partes do teste ou do app sendo testado.

A função waitForIdle() aguarda que o Compose esteja inativo, mas a função depende da propriedade autoAdvance:

composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.

composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.

Nos dois casos, waitForIdle() também aguarda as transmissões de layout e desenho pendentes.

Além disso, você pode avançar o relógio até que uma determinada condição seja atendida com advanceTimeUntil().

composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }

A condição especificada precisa ser verificar o estado que pode ser afetado por esse relógio. Isso só funciona com estados no Compose.

Otimizar testes de animação

When testing high-fidelity animations, you often need to disable auto-advance and manually step through frames to assert intermediate UI states. For these specific frame-by-frame loops, use the runWithoutImplicitWait method to execute your assertions. Standard node queries (like onNodeWithTag or fetchSemanticsNode) trigger implicit synchronizations that are redundant when you are manually controlling the clock, so bypassing them significantly speeds up your test runtimes.

Usage guidelines

  • Manual clock management: Use this API when mainClock.autoAdvance is set to false and the UI is in a known, stable state for the current frame.
  • UI thread execution: To ensure the stability of the UI tree, call runWithoutImplicitWait on the UI thread, such as with runOnUiThread. Running it off the UI thread exposes your test to race conditions and stale state reads.
  • Read-only assertions: The block should strictly contain read-only assertions. Any actions that mutate state should be performed outside of this block.

Example

@Test
fun runWithoutImplicitWaitSample() = runComposeUiTest {
    setContent { MainScreen() }
    mainClock.autoAdvance = false

    // Trigger an animation
    onNodeWithText("Start Animation").performClick()

    // Step through the animation frame-by-frame
    while (hasPendingWork()) {
        mainClock.advanceTimeByFrame()
        waitForIdle()
        runOnUiThread {
            // Suppress implicit synchronization inside this block to avoid redundant
            // waits on each node query, making the frame assertions execute much faster.
            runWithoutImplicitWait {
                val box1 = onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = onNodeWithTag("Box2").fetchSemanticsNode()
                val box3 = onNodeWithTag("Box3").fetchSemanticsNode()

                // Assert the exact intermediate state of all three properties for this frame
                assert(box1.boundsInRoot.right <= box2.boundsInRoot.left)
                assert(box2.boundsInRoot.right <= box3.boundsInRoot.left)
            }
        }
    }
}

Sincronização da linha de execução principal

Os testes do Compose agora oferecem suporte à sincronização da linha de execução principal, permitindo que você chame waitForIdle e, por extensão, ações e declarações da IU do Compose diretamente da linha de execução principal.

Anteriormente, os testes do Compose aplicavam um modelo de duas linhas de execução: a execução do teste ocorria em uma linha de execução de teste em segundo plano, enquanto as atualizações da IU aconteciam na linha de execução principal. Chamar métodos de sincronização como waitForIdle ou runOnIdle da linha de execução principal (por exemplo, dentro de um bloco runOnUiThread) geraria uma IllegalStateException porque o framework aplicava verificações de linha de execução rigorosas para evitar a sincronização da linha de execução principal.

Com a sincronização da linha de execução principal ativada, o framework de teste do Compose agora pode avançar o relógio e processar o trabalho pendente, mesmo quando chamadas de bloqueio são feitas na linha de execução principal.

Quando usar a sincronização da linha de execução principal

Embora manter os testes na linha de execução em segundo plano continue sendo o padrão para testes puros do Compose, a sincronização da linha de execução principal é altamente vantajosa em alguns cenários específicos:

  • Interoperabilidade complexa de visualização: ao testar IUs híbridas que contêm o Compose e visualizações legadas do Android, a manipulação de visualizações geralmente exige a execução na linha de execução principal. Agora você pode interagir com visualizações e declarar nós do Compose sequencialmente sem alternar constantemente os contextos de linha de execução.
  • Mutações de estado síncronas: se sua arquitetura depende de detentores de estado estritamente vinculados à linha de execução principal, agora você pode mudar o estado e aguardar imediatamente que a IU do Compose seja resolvida sem sair da linha de execução principal.
  • Executores de teste personalizados: se você estiver criando uma infraestrutura de teste personalizada ou usando ambientes em que o executor de teste é executado inerentemente na linha de execução principal, os testes do Compose agora serão executados de maneira limpa sem exigir delegação de linha de execução em segundo plano.

Exemplo

Historicamente, como a sincronização era estritamente proibida na linha de execução principal, os desenvolvedores precisavam alternar entre a linha de execução do executor de teste em segundo plano e a linha de execução de interface, o que levava a testes desconexos:

@Test
fun testBidirectionalInteropUIUpdates_old() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
    }
    // Jump to Test Thread to verify state settles inside compose
    composeTestRule.waitForIdle()
    composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
    composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
    composeTestRule.waitForIdle()
    // Jump back to Main Thread to verify target view state settles
    scenario.onFragment { fragment ->
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

Com a sincronização da linha de execução principal ativada, as declarações para hierarquias do Compose e de visualização podem ser executadas no mesmo bloco:

@Test
fun testBidirectionalInteropUIUpdates_new() {
    val scenario = launchFragmentInContainer<InteropFragment>()
    composeTestRule.waitForIdle()
    scenario.onFragment { fragment ->
        fragment.legacyButton.performClick()
        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed()
        composeTestRule.onNodeWithText("Increment Legacy TextView").performClick()
        composeTestRule.waitForIdle()
        assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1")
    }
}

Aguardar condições

Qualquer condição que dependa de trabalho externo, como carregamento de dados ou medidas ou desenhos do Android (ou seja, medidas ou desenhos externos ao Compose), precisa usar um conceito mais geral, como waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

Você também pode usar qualquer um dos waitUntil auxiliares:

composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)

composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)

composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)

composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)

Outros recursos

  • Testar apps no Android: a página de destino principal de testes do Android oferece uma visão mais ampla dos princípios básicos e técnicas de teste.
  • Princípios básicos para testes: Saiba mais sobre os conceitos básicos por trás do teste de um app Android.
  • Testes locais: é possível executar alguns testes localmente, na sua estação de trabalho.
  • Testes de instrumentação: também é recomendável executar testes de instrumentação. Ou seja, testes executados diretamente no dispositivo.
  • Integração contínua: A integração contínua permite integrar seus testes ao pipeline de implantação.
  • Testar diferentes tamanhos de tela: Com tantos dispositivos disponíveis para os usuários, é recomendável testar diferentes tamanhos de tela.
  • Espresso: embora seja destinado a IUs baseadas em visualização, o conhecimento do Espresso ainda pode ser útil para alguns aspectos dos testes do Compose.