O teste de IUs ou de telas é usado para verificar o comportamento correto do código do Compose, melhorando a qualidade do app ao detectar erros no início do processo de desenvolvimento.
O Compose fornece um conjunto de APIs de teste para encontrar elementos, verificar atributos e executar ações do usuário. As APIs também incluem recursos avançados, como manipulação de tempo.
Semântica
Os testes de IU no Compose usam semântica para interagir com a hierarquia da IU. A semântica, como o nome indica, dá um significado para uma parte da IU. Nesse contexto, uma "parte da IU" (ou elemento) pode significar qualquer coisa, desde um único elemento combinável até uma tela cheia. A árvore semântica é gerada junto com a hierarquia de IU e a descreve.
Figura 1. Uma hierarquia de IU típica e a árvore de semântica dela.
O framework de semântica é usado principalmente para acessibilidade. Portanto, os testes aproveitam as informações expostas pela semântica sobre a hierarquia da IU. Os desenvolvedores decidem o que e quanto precisa ser exposto.
Figura 2. Um botão típico com um ícone e um texto.
Por exemplo, considerando um botão como este, que consiste em um ícone e um elemento
de texto, a árvore semântica padrão contém apenas o rótulo de texto "Like". Isso
ocorre porque alguns elementos que podem ser compostos, como Text
, já expõem algumas propriedades
à árvore semântica. É possível adicionar propriedades à árvore semântica usando um
Modifier
.
MyButton(
modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)
Configurar
Esta seção descreve como configurar seu módulo para permitir que você teste o código do Compose.
Primeiro, adicione as seguintes dependências ao arquivo build.gradle
do módulo
que contém seus testes de IU:
// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createAndroidComposeRule, but not createComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
Esse módulo inclui uma ComposeTestRule
e uma implementação para Android
chamada AndroidComposeTestRule
. Com essa regra, é possível definir o conteúdo do Compose
ou acessar a atividade. O teste de IU típico do Compose tem esta aparência:
// file: app/src/androidTest/java/com/package/MyComposeTest.kt
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
// use createAndroidComposeRule<YourActivity>() if you need access to
// an activity
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = fakeUiState, /*...*/)
}
}
composeTestRule.onNodeWithText("Continue").performClick()
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}
}
Como testar APIs
Existem três maneiras principais de interagir com os elementos:
- Os finders permitem selecionar um ou vários elementos (ou nós na árvore semântica) para fazer declarações ou executar ações.
- As declarações são usadas para verificar se os elementos existem ou têm determinados atributos.
- As ações injetam eventos de usuário simulados nos elementos, como cliques ou outros gestos.
Algumas APIs aceitam um
SemanticsMatcher
para se referir a um ou mais nós na árvore semântica.
Finders
É possível usar onNode
e onAllNodes
para selecionar um ou vários nós, respectivamente.
Você também pode usar finders de conveniência para as pesquisas mais comuns, como
onNodeWithText
,
onNodeWithContentDescription
etc.
A lista completa está disponível na Folha de referência de testes do Compose.
Selecionar um único nó
composeTestRule.onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction
// Example
composeTestRule
.onNode(hasText("Button")) // Equivalent to onNodeWithText("Button")
Selecionar vários nós
composeTestRule
.onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
// Example
composeTestRule
.onAllNodes(hasText("Button")) // Equivalent to onAllNodesWithText("Button")
Como usar a árvore não mesclada
Alguns nós mesclam informações de semântica dos filhos deles. Por exemplo, um botão com dois elementos de texto mescla os rótulos:
MyButton {
Text("Hello")
Text("World")
}
Em um teste, podemos usar printToLog()
para exibir a árvore semântica:
composeTestRule.onRoot().printToLog("TAG")
Esse código gera a seguinte saída:
Node #1 at (...)px
|-Node #2 at (...)px
Role = 'Button'
Text = '[Hello, World]'
Actions = [OnClick, GetTextLayoutResult]
MergeDescendants = 'true'
Se você precisar fazer a correspondência de um nó do que seria a árvore não mesclada, defina
useUnmergedTree
como true
:
composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")
Esse código gera a seguinte saída:
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = '[Hello]'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = '[World]'
O parâmetro useUnmergedTree
está disponível em todos os finders. Por exemplo,
aqui ele é usado em um finder onNodeWithText
.
composeTestRule
.onNodeWithText("World", useUnmergedTree = true).assertIsDisplayed()
Declarações
Verifique as declarações chamando assert()
na SemanticsNodeInteraction
retornada por um finder com um ou vários matchers:
// Single matcher:
composeTestRule
.onNode(matcher)
.assert(hasText("Button")) // hasText is a SemanticsMatcher
// Multiple matchers can use and / or
composeTestRule
.onNode(matcher).assert(hasText("Button") or hasText("Button2"))
Também é possível usar funções de conveniência para as declarações mais comuns, como
assertExists
,
assertIsDisplayed
,
assertTextEquals
,
entre outras. A lista completa pode ser encontrada na Folha de referência de testes no Compose.
Também há funções para verificar declarações em uma coleção de nós:
// Check number of matched nodes
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertCountEquals(4)
// At least one matches
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAny(hasTestTag("Drummer"))
// All of them match
composeTestRule
.onAllNodesWithContentDescription("Beatle").assertAll(hasClickAction())
Ações
Para injetar uma ação em um nó, chame uma função perform…()
:
composeTestRule.onNode(...).performClick()
Veja alguns exemplos de ações:
performClick(),
performSemanticsAction(key),
performKeyPress(keyEvent),
performGesture { swipeLeft() }
A lista completa pode ser encontrada na Folha de referência de testes no Compose.
Matchers
Esta seção descreve alguns dos matchers disponíveis para testes do código do Compose.
Matchers hierárquicos
Os matchers hierárquicos permitem subir ou descer na árvore semântica e realizar correspondências simples.
fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher): SemanticsMatcher
Veja alguns exemplos desses matchers em uso:
composeTestRule.onNode(hasParent(hasText("Button")))
.assertIsDisplayed()
Seletores
Uma forma alternativa de criar testes é usando seletores, que podem deixar alguns testes mais legíveis.
composeTestRule.onNode(hasTestTag("Players"))
.onChildren()
.filter(hasClickAction())
.assertCountEquals(4)
.onFirst()
.assert(hasText("John"))
A lista completa pode ser encontrada na Folha de referência de testes no Compose.
Sincronização
Por padrão, os testes do Compose são sincronizados com sua IU. Quando você chama uma
declaração ou uma ação usando ComposeTestRule
, o teste vai ser 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()
}
Também é importante observar que 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 de inatividade
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.
É possível criar e registrar 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.
waitForIdle
aguarda que o Compose esteja 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 aguardará
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. Qualquer condição que dependa de
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 }
Padrões comuns
Esta seção descreve algumas abordagens comuns que você verá nos testes do Compose.
Testar de forma isolada
A ComposeTestRule
permite que você
inicie uma atividade mostrando qualquer elemento de composição, como o app inteiro, uma única
tela ou um elemento pequeno. Também é aconselhável verificar se os elementos que podem ser compostos
estão encapsulados corretamente e se funcionam de forma independente, permitindo testes de IU mais fáceis
e mais focados.
Isso não significa que você só pode criar testes de IU de unidade. Os testes voltados para partes maiores da IU também são muito importantes.
Acessar a atividade e os recursos depois de definir seu conteúdo
Muitas vezes, é necessário definir o conteúdo que está em teste usando
composeTestRule.setContent
e também acessar recursos de atividade,
por exemplo, para declarar que um texto mostrado corresponde a um recurso de string. No entanto,
não vai ser possível chamar o setContent
em uma regra criada com
createAndroidComposeRule()
se ele já tiver sido chamado pela atividade.
Um padrão comum para fazer isso é criar uma AndroidComposeTestRule
usando
uma atividade vazia,
como ComponentActivity
.
class MyComposeTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun myTest() {
// Start the app
composeTestRule.setContent {
MyAppTheme {
MainScreen(uiState = exampleUiState, /*...*/)
}
}
val continueLabel = composeTestRule.activity.getString(R.string.next)
composeTestRule.onNodeWithText(continueLabel).performClick()
}
}
Observe que a ComponentActivity
precisa ser adicionada ao arquivo
AndroidManifest.xml
do app. Você pode fazer isso adicionando esta dependência ao
módulo:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
Propriedades de semântica personalizadas
É possível criar propriedades de semântica personalizadas para expor informações aos testes. Para fazer isso,
defina uma nova SemanticsPropertyKey
e disponibilize-a usando o
SemanticsPropertyReceiver
.
// Creates a Semantics property of type boolean
val PickedDateKey = SemanticsPropertyKey<Long>("PickedDate")
var SemanticsPropertyReceiver.pickedDate by PickedDateKey
Agora, você pode usar essa propriedade usando o modificador semantics
:
val datePickerValue by remember { mutableStateOf(0L) }
MyCustomDatePicker(
modifier = Modifier.semantics { pickedDate = datePickerValue }
)
Após os testes, você pode usar SemanticsMatcher.expectValue
para declarar o valor
da propriedade:
composeTestRule
.onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21
.assertExists()
Verificar a restauração do estado
Verifique se o estado dos elementos do Compose é restaurado corretamente
quando a atividade ou o processo são recriados. É possível fazer essa verificação com a classe
StateRestorationTester
sem depender da recriação de
atividades.
Essa classe permite simular a recriação de um elemento de composição. Ela é
especialmente útil para verificar a implementação de
rememberSaveable
.
class MyStateRestorationTests {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun onRecreation_stateIsRestored() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent { MainScreen() }
// TODO: Run actions that modify the state
// Trigger a recreation
restorationTester.emulateSavedInstanceStateRestore()
// TODO: Verify that state has been correctly restored.
}
}
Depurar
A principal maneira de resolver problemas nos testes é observar a árvore semântica.
Você pode gerar a árvore chamando composeTestRule.onRoot().printToLog()
a qualquer momento no
teste. Essa função gera um registro como este:
Node #1 at (...)px
|-Node #2 at (...)px
OnClick = '...'
MergeDescendants = 'true'
|-Node #3 at (...)px
| Text = 'Hi'
|-Node #5 at (83.0, 86.0, 191.0, 135.0)px
Text = 'There'
Esses registros contêm informações importantes para encontrar bugs.
Interoperabilidade com o Espresso
Em um app híbrido, é possível encontrar componentes do Compose dentro de hierarquias de visualização e
visualizações dentro dos elementos do Compose (com o elemento AndroidView
).
Não há etapas específicas que sejam necessárias para fazer correspondência a nenhum dos tipos. A correspondência de visualizações é feita usando o método
onView
do Espresso e os elementos do Compose com ComposeTestRule
.
@Test
fun androidViewInteropTest() {
// Check the initial state of a TextView that depends on a Compose state:
Espresso.onView(withText("Hello Views")).check(matches(isDisplayed()))
// Click on the Compose button that changes the state
composeTestRule.onNodeWithText("Click here").performClick()
// Check the new value
Espresso.onView(withText("Hello Compose")).check(matches(isDisplayed()))
}
Interoperabilidade com o UiAutomator
Por padrão, os elementos de composição podem ser acessados no
UiAutomator apenas pelos
descritores convenientes (texto mostrado, descrição do conteúdo etc.). Caso queira
acessar um elemento combinável que use Modifier.testTag
, é necessário ativar a
propriedade semântica testTagAsResourceId
para a subárvore de elementos combináveis específicos.
Ativar esse comportamento é útil para elementos de composição que não têm nenhum outro
elemento exclusivo, por exemplo, elementos roláveis como a LazyColumn
.
Só é possível ativar o recurso uma vez na hierarquia dos elementos de composição para garantir que todos os
elementos de composição aninhados com Modifier.testTag
possam ser acessados pelo UiAutomator.
Scaffold(
// Enables for all composables in the hierarchy.
modifier = Modifier.semantics {
testTagsAsResourceId = true
}
){
// Modifier.testTag is accessible from UiAutomator for composables nested here.
LazyColumn(
modifier = Modifier.testTag("myLazyColumn")
){
// content
}
}
Todos os elementos de composição com Modifier.testTag(tag)
podem ser acessados com
By.res(resourceName)
usando a mesma tag
do resourceName
.
val device = UiDevice.getInstance(getInstrumentation())
val lazyColumn: UiObject2 = device.findObject(By.res("myLazyColumn"))
// some interaction with the lazyColumn
Saiba mais
Para saber mais, consulte o codelab sobre como testar no Jetpack Compose.