A natureza assíncrona dos aplicativos e frameworks para dispositivos móveis muitas vezes dificulta a criação de testes confiáveis e repetíveis. Quando um evento do usuário é injetado, o framework de teste precisa esperar que o app termine de reagir a ele, o que pode variar desde a alteração de algum texto na tela até uma recriação completa de uma atividade. Quando um teste não tem um comportamento determinístico, ele é instável.
Frameworks modernos, como o Compose ou o Espresso, são projetados pensando em testes, portanto, há uma certa garantia de que a interface ficará inativa antes da próxima ação ou declaração de teste. Isso é a sincronização.
Testar a sincronização
Os problemas ainda podem surgir quando você executa operações assíncronas ou em segundo plano desconhecidas ao teste, como carregar dados de um banco de dados ou mostrar animações infinitas.
Para aumentar a confiabilidade do seu pacote de testes, é possível instalar uma maneira de rastrear operações em segundo plano, como os recursos de inatividade do Espresso. Além disso, é possível substituir módulos para testar versões que podem ser consultadas para inatividade ou que melhoram a sincronização, como TestDispatcher para corrotinas ou RxIdler para RxJava.
Maneiras de melhorar a estabilidade
Testes grandes podem capturar muitas regressões ao mesmo tempo porque testam vários componentes de um app. Normalmente, eles são executados em emuladores ou dispositivos, o que significa que eles têm alta fidelidade. Embora grandes testes completos forneçam uma cobertura abrangente, eles são mais propensos a falhas ocasionais.
As principais medidas que você pode tomar para reduzir a instabilidade são as seguintes:
- Configurar os dispositivos corretamente
- Evitar problemas de sincronização
- Implementar novas tentativas
Para criar testes grandes usando o Compose ou o Espresso, normalmente você inicia uma das atividades e navega como um usuário, verificando se a interface se comporta corretamente usando declarações ou testes de captura de tela.
Outros frameworks, como o UI Automator, permitem um escopo maior, já que é possível interagir com a interface do sistema e outros apps. No entanto, os testes do UI Automator podem exigir mais sincronização manual, portanto, eles tendem a ser menos confiáveis.
Configurar dispositivos
Primeiro, para melhorar a confiabilidade dos testes, verifique se o sistema operacional do dispositivo não interrompe inesperadamente a execução dos testes. Por exemplo, quando uma caixa de diálogo de atualização do sistema é mostrada sobre outros apps ou quando o espaço em disco é insuficiente.
Os provedores de farms de dispositivos configuram os dispositivos e emuladores. Normalmente, não é necessário fazer nada. No entanto, eles podem ter próprias diretivas de configuração para casos especiais.
Dispositivos gerenciados pelo Gradle
Se você gerencia emuladores por conta própria, pode usar dispositivos gerenciados pelo Gradle para definir quais dispositivos usar para executar seus testes:
android {
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
// Use device profiles you typically see in Android Studio.
device = "Pixel 2"
// Use only API levels 27 and higher.
apiLevel = 30
// To include Google services, use "google".
systemImageSource = "aosp"
}
}
}
}
}
Com essa configuração, o comando a seguir vai criar uma imagem do emulador, iniciar uma instância, executar os testes e encerrar.
./gradlew pixel2api30DebugAndroidTest
Os dispositivos gerenciados pelo Gradle contêm mecanismos para tentar novamente em caso de desconexões e outras melhorias.
Evitar problemas de sincronização
Componentes que executam operações em segundo plano ou assíncronas podem levar a falhas porque uma instrução de teste foi executada antes que a interface estivesse pronta. À medida que o escopo de um teste aumenta, as chances de ele se tornar instável também aumentam. Esses problemas de sincronização são uma fonte principal de inconsistência, porque os frameworks de teste precisam deduzir se uma atividade está sendo carregada concluída ou se ela precisa esperar mais.
Soluções
É possível usar os recursos de inatividade do Espresso para indicar quando um app está ocupado, mas é difícil rastrear cada operação assíncrona, especialmente em testes de ponta a ponta muito grandes. Além disso, pode ser difícil instalar recursos ociosos sem poluir o código em teste.
Em vez de estimar se uma atividade está ocupada ou não, você pode fazer com que os testes aguardem até que condições específicas sejam atendidas. Por exemplo, você pode esperar até que um texto ou componente específico seja mostrado na interface.
O Compose tem uma coleção de APIs de teste como parte do
ComposeTestRule
para aguardar diferentes comparadores:
fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilExactlyOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)
fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)
E uma API genérica que usa qualquer função que retorna um booleano:
fun waitUntil(timeoutMillis: Long, condition: () -> Boolean): Unit
Exemplo de uso:
composeTestRule.waitUntilExactlyOneExists(hasText("Continue")</code>)</p></td>
Mecanismos de nova tentativa
É preciso corrigir testes instáveis, mas às vezes as condições que os fazem falhar são tão improváveis que são difíceis de reproduzir. Embora seja necessário sempre monitorar e corrigir testes instáveis, um mecanismo de nova tentativa pode ajudar a manter a produtividade do desenvolvedor executando o teste várias vezes até que ele seja aprovado.
As novas tentativas precisam acontecer em vários níveis para evitar problemas, como:
- A conexão com o dispositivo expirou ou foi perdida
- Falha em um único teste
A instalação ou configuração de novas tentativas depende dos frameworks de teste e da infraestrutura, mas os mecanismos típicos incluem:
- Uma regra do JUnit que tenta executar qualquer teste várias vezes
- Uma ação ou etapa de repetição no fluxo de trabalho de CI
- Um sistema para reiniciar um emulador quando ele não responde, como dispositivos gerenciados pelo Gradle.