Conceitos básicos para testes de apps Android

Esta página descreve os princípios básicos dos testes de apps Android, incluindo as práticas recomendadas principais e os benefícios delas.

Benefícios dos testes

Os testes são uma parte importante do processo de desenvolvimento de apps. Ao executar testes no app de forma consistente, você pode verificar a precisão, o comportamento funcional e a usabilidade dele antes de lançá-lo publicamente.

É possível testar seu app manualmente. Você pode usar dispositivos e emuladores diferentes, mudar o idioma do sistema e tentar gerar cada erro de usuário ou percorrer todos os fluxos de usuário.

No entanto, o teste manual é mal escalonado e pode ser fácil ignorar regressões no comportamento do app. Os testes automatizados envolvem o uso de ferramentas que realizam testes para você, que são mais rápidas, repetíveis e geralmente oferecem um feedback mais útil sobre o app no início do processo de desenvolvimento.

Tipos de testes no Android

Os aplicativos para dispositivos móveis são complexos e precisam funcionar bem em muitos ambientes. Por isso, há muitos tipos de testes.

Assunto

Por exemplo, há diferentes tipos de teste, dependendo do assunto:

  • Teste funcional: meu app faz o que deveria?
  • Teste de desempenho: é rápido e eficiente?
  • Teste de acessibilidade: ele funciona bem com serviços de acessibilidade?
  • Teste de compatibilidade: ele funciona bem em todos os dispositivos e níveis de API?

Escopo

Os testes também variam de acordo com o tamanho ou o grau de isolamento:

  • Os testes de unidade ou testes pequenos verificam apenas uma parte muito pequena do app, como um método ou uma classe.
  • Os testes completos ou testes grandes verificam partes maiores do app ao mesmo tempo, como uma tela inteira ou o fluxo de usuários.
  • Os testes médios ocorrem nesse meio e verificam a integração entre duas ou mais unidades.
Os testes podem ser pequenos, médios ou grandes.
Figura 1: escopos de teste em um aplicativo comum.

Há muitas maneiras de classificar testes. No entanto, a distinção mais importante para desenvolvedores de apps é onde os testes são executados.

Comparação entre testes instrumentados e locais

É possível executar testes em um dispositivo Android ou em outro computador:

  • Os testes de instrumentação são executados em um dispositivo Android, físico ou emulado. O app é criado e instalado com um app de teste que injeta comandos e lê o estado. Os testes de instrumentação geralmente são testes de interface, que iniciam um app e interagem com ele.
  • Os testes locais são executados na máquina de desenvolvimento ou em um servidor. Portanto, também são chamados de testes no lado do host. Geralmente, eles são pequenos e rápidos, isolando o objeto em teste do restante do app.
Os testes podem ser executados como testes instrumentados em um dispositivo ou testes locais na máquina de desenvolvimento.
Figura 2: diferentes tipos de testes dependendo de onde eles são executados.

Nem todos os testes de unidade são locais, e nem todos os testes de ponta a ponta são executados em um dispositivo. Por exemplo:

  • Grande teste local: use um simulador do Android executado localmente, como o Robolectric (em inglês).
  • Pequeno teste de instrumentação: você pode verificar se o código funciona bem com um recurso de framework, como um banco de dados SQLite. Você pode executar esse teste em vários dispositivos para verificar a integração com várias versões do SQLite.

Exemplos

Os snippets a seguir demonstram como interagir com a interface em um teste de interface instrumentado, que clica em um elemento e verifica se outro elemento está sendo exibido.

Espresso

// When the Continue button is clicked
onView(withText("Continue"))
    .perform(click())

// Then the Welcome screen is displayed
onView(withText("Welcome"))
    .check(matches(isDisplayed()))

Compose UI

// When the Continue button is clicked
composeTestRule.onNodeWithText("Continue").performClick()

// Then the Welcome screen is displayed
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()

O snippet abaixo mostra parte de um teste de unidade para um ViewModel (teste local no lado do host):

// Given an instance of MyViewModel
val viewModel = MyViewModel(myFakeDataRepository)

// When data is loaded
viewModel.loadData()

// Then it should be exposing data
assertTrue(viewModel.data != null)

Como definir uma estratégia de teste

Em um mundo ideal, você testaria todas as linhas de código do app em todos os dispositivos compatíveis com ele. Infelizmente, essa abordagem é muito lenta e cara para ser prática.

Uma boa estratégia de teste encontra um equilíbrio adequado entre a fidelidade, a velocidade e a confiabilidade de um teste. A semelhança do ambiente de teste com um dispositivo real determina a fidelidade do teste. Testes de fidelidade mais altos são executados em dispositivos emulados ou no próprio dispositivo físico. Testes de fidelidade mais baixos podem ser executados na JVM da estação de trabalho local. Os testes de alta fidelidade costumam ser mais lentos e exigem mais recursos. Portanto, nem todos os testes precisam ser de alta fidelidade.

Testes instáveis

Erros ocorrem mesmo em execuções de teste projetadas e implementadas corretamente. Por exemplo, ao executar um teste em um dispositivo real, uma atualização automática pode começar no meio de um teste e causar uma falha. Disputas sutis no código podem ocorrer apenas em uma pequena porcentagem do tempo. Os testes que não passam em 100% do tempo são instáveis.

Arquitetura testável

Com uma arquitetura de app testável, o código segue uma estrutura que permite testar facilmente diferentes partes dele isoladamente. As arquiteturas testáveis têm outras vantagens, como melhor legibilidade, manutenção, escalonabilidade e reutilização.

Uma arquitetura não testável produz o seguinte:

  • Testes maiores, mais lentos e instáveis. As classes que não podem ser testadas na unidade precisam ser cobertas por testes de integração ou de interface maiores.
  • Menos oportunidades para testar cenários diferentes. Testes maiores são mais lentos, portanto, testar todos os estados possíveis de um app pode ser irreal.

Para saber mais sobre as diretrizes de arquitetura, consulte o guia para arquitetura de apps.

Abordagens para desacoplamento

Se você conseguir extrair parte de uma função, classe ou módulo do resto, os testes serão mais fáceis e eficazes. Essa prática é conhecida como desacoplamento e é o conceito mais importante para a arquitetura testável.

As técnicas comuns de desacoplamento incluem:

  • Divida um app em camadas, como Apresentação, Domínio e Dados. Também é possível dividir um app em módulos, um por recurso.
  • Evite adicionar lógica a entidades que têm grandes dependências, como atividades e fragmentos. Use essas classes como pontos de entrada para o framework e mova a lógica de interface e negócios para outro lugar, como uma camada combinável, de ViewModel ou de domínio.
  • Evite dependências de framework diretas em classes que contêm lógica de negócios. Por exemplo, não use os contextos do Android em ViewModels.
  • Facilitar a substituição das dependências. Por exemplo, use interfaces em vez de implementações concretas. Use a Injeção de dependência mesmo que você não use um framework de DI.

Próximas etapas

Agora que você sabe por que testar e os dois principais tipos de teste, leia O que testar.

Como alternativa, se você quiser criar seu primeiro teste e aprender na prática, confira os codelabs sobre como testar.