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.autoAdvanceestiver definido comofalsee 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
runWithoutImplicitWaitna linha de execução de interface, como comrunOnUiThread. 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.