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 o 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.

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 em teste está ocupado ou inativo. Não é necessário fazer nada, a menos que você precise registrar outros recursos de inatividade, por exemplo, se você executar um job em segundo plano que não esteja sincronizado com o Espresso ou o Compose.

Essa API é muito semelhante aos recursos de inatividade do Espresso para indicar se o objeto em teste está inativo ou ocupado. Use a regra de teste do Compose para registrar a implementação do 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 o Compose ficar inativo, mas 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, o waitForIdle() também aguarda as passagens de desenho e layout 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

Ao testar animações de alta fidelidade, geralmente é necessário desativar o avanço automático e percorrer os frames manualmente para declarar estados intermediários da interface. Para esses loops específicos de frame a frame, use o método runWithoutImplicitWait para executar as declarações. As consultas de nó padrão (como onNodeWithTag ou fetchSemanticsNode) acionam sincronizações implícitas que são redundantes quando você controla o relógio manualmente. Portanto, ignorá-las acelera significativamente os tempos de execução do teste.

diretrizes de uso

  • Gerenciamento manual do relógio: use essa API quando mainClock.autoAdvance estiver definido como false e a interface estiver em um estado conhecido e estável para o frame atual.
  • Execução da linha de execução de interface: para garantir a estabilidade da árvore de interface, chame runWithoutImplicitWait na linha de execução de interface, como com runOnUiThread. Executar o teste fora da linha de execução de interface expõe o teste a disputas e leituras de estado desatualizadas.
  • Declarações somente leitura: o bloco precisa conter estritamente declarações somente leitura. Todas as ações que modificam o estado precisam ser realizadas fora desse bloco.

Exemplo

@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 são compatíveis com a sincronização da linha de execução principal, permitindo que você chame waitForIdle com segurança, além de ações e declarações da interface do Compose, diretamente da linha de execução principal.

Antes, o teste do Compose aplicava estritamente 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 interface aconteciam na linha de execução principal. Chamar métodos de sincronização como waitForIdle ou runOnIdle na linha de execução principal (por exemplo, dentro de um bloco runOnUiThread) geraria uma IllegalStateException, porque o framework aplicava verificações rigorosas de linha de execução 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 é muito vantajosa em alguns cenários específicos:

  • Interoperabilidade de visualização complexa: ao testar interfaces híbridas que contêm o Compose e as visualizações legadas do Android, a manipulação de visualizações geralmente requer a execução na linha de execução principal. Agora é possível interagir com visualizações e fazer declarações em nós do Compose sequencialmente sem alternar constantemente os contextos de linha de execução.
  • Mutações de estado síncronas: se a arquitetura depende de detentores de estado estritamente vinculados à linha de execução principal, agora é possível mudar o estado e aguardar imediatamente que a interface do Compose se estabilize 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 de forma inerente na linha de execução principal, os testes do Compose agora são executados de forma limpa, sem exigir a 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 testes em segundo plano e a linha de execução de interface, o que resultava em 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 as hierarquias do Compose e da 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 medição ou desenho do Android (ou seja, medição ou desenho externo ao Compose), precisa usar um conceito mais geral, como waitUntil():

composeTestRule.waitUntil(timeoutMs) { condition }

Também é possível usar qualquer um dos auxiliares do waitUntil:

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 conceitos básicos e das técnicas de teste.
  • Conceitos básicos de 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 própria estação de trabalho.
  • Testes de instrumentação:também é uma boa prática executar testes de instrumentação. Ou seja, testes que são executados diretamente no dispositivo.
  • Integração contínua:com a integração contínua, é possível integrar seus testes ao pipeline de implantação.
  • Teste diferentes tamanhos de tela:com tantos dispositivos disponíveis para os usuários, é importante testar diferentes tamanhos de tela.
  • Espresso: embora seja destinado a interfaces baseadas em visualizações, o conhecimento do Espresso ainda pode ser útil para alguns aspectos dos testes do Compose.