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 interface 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 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 de 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 fique 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.
Em ambos os casos, waitForIdle() também aguarda 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
Ao testar animações de alta fidelidade, muitas vezes é 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 frame a frame, use o runWithoutImplicitWait método para
executar suas declarações. Consultas de nós padrão (como onNodeWithTag ou
fetchSemanticsNode) acionam sincronizações implícitas que são redundantes
quando você está controlando 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 da interface: para garantir a estabilidade da árvore de interface, chame
runWithoutImplicitWaitna linha de execução da interface, como comrunOnUiThread. A execução fora da linha de execução da interface expõe seu teste a condições de corrida e leituras de estado obsoletas. - Declarações somente leitura: o bloco precisa conter 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
O teste do Compose agora oferece suporte à sincronização da linha de execução principal, permitindo que você chame waitForIdle (e, por extensão, ações e asserções da interface do Compose) diretamente da linha de execução principal.
Antes, os testes do Compose aplicavam 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
da linha de execução principal (por exemplo, dentro de um bloco runOnUiThread) geraria
um IllegalStateException porque o framework impôs verificações estritas de linhas 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 complexa de visualizações: ao testar interfaces híbridas que contêm visualizações do Compose e do Android legadas, a manipulação de visualizações geralmente exige a execução na linha de execução principal. Agora você pode interagir com Views e fazer asserções em nós do Compose sequencialmente sem trocar constantemente de contextos de linhas de execução.
- Mutações de estado síncronas: se sua arquitetura depender de titulares de estado estritamente vinculados à linha de execução principal, agora você poderá mudar o estado e esperar imediatamente que a interface do Compose seja concluída 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 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 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 hierarquias do Compose e do View 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
assistentes 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 princípios básicos e das técnicas de teste.
- Conceitos básicos de testes:saiba mais sobre os principais conceitos 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 instrumentados:é uma boa prática executar também testes instrumentados. Ou seja, testes executados diretamente no dispositivo.
- Integração contínua:permite 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ção, o conhecimento do Espresso ainda pode ser útil para alguns aspectos dos testes do Compose.