Usar cópias de teste no Android

Ao projetar a estratégia de teste para um elemento ou sistema, há três aspectos de teste relacionados:

  • Escopo: quanto do código o teste toca? Os testes podem verificar um único método, o aplicativo inteiro ou algum intermediário. O escopo testado está em teste e geralmente se refere a ele como Assunto em teste, além de Sistema em teste ou Unidade em teste.
  • Velocidade: com que rapidez o teste é executado? As velocidades de teste podem variar de milissegundos a vários minutos.
  • Fidelidade: o quão realista é o teste? Por exemplo, se parte do código que você está testando precisa fazer uma solicitação de rede, o código de teste realmente faz essa solicitação de rede ou ele falsifica o resultado? Se o teste realmente se comunica com a rede, isso significa que ela tem maior fidelidade. A desvantagem é que o teste pode levar mais tempo para ser executado, resultar em erros se a rede estiver inativa ou poder ser caro para ser usado.

Veja o que testar para saber como começar a definir sua estratégia de teste.

Isolamento e dependências

O teste de um elemento ou sistema de elementos é feito em isolamento. Por exemplo, para testar um ViewModel, não é necessário iniciar um emulador e iniciar uma interface, porque isso não depende (ou não precisa) do framework do Android.

No entanto, o objeto em teste pode depender de outros para funcionar. Por exemplo, um ViewModel pode depender de um repositório de dados para funcionar.

Quando é necessário fornecer uma dependência a um objeto em teste, uma prática comum é criar um teste duplo (ou objeto de teste). As cópias de teste são objetos que parecem e atuam como componentes no app, mas são criados no teste para fornecer um comportamento ou dados específicos. As principais vantagens são que eles tornam os testes mais rápidos e simples.

Tipos de testes duplos

Há vários tipos de testes duplos:

Conteúdo falso Um teste duplo que tem uma implementação "funcional" da classe, mas é implementada de forma a torná-la boa para testes, mas inadequada para produção.

Exemplo: um banco de dados na memória.

As falsificações não exigem uma estrutura de simulação e são leves. Eles são preferidos.

Modelo Uma dupla de teste que se comporta como você a programa para se comportar e que cria expectativas sobre as interações. As simulações vão falhar nos testes se as interações não corresponderem aos requisitos que você definiu. As simulações geralmente são criadas com um framework de simulação para fazer tudo isso.

Exemplo: verifique se um método em um banco de dados foi chamado exatamente uma vez.

Stub Um teste duplo que se comporta como você o programa para se comportar, mas não tem expectativas sobre as interações dele. Geralmente criado com um framework de simulação. Por questões de simplicidade, é melhor usar falsificações em vez de stubs.
Falso Um duplo de teste que é transmitido, mas não usado, por exemplo, se você só precisar fornecê-lo como um parâmetro.

Exemplo: uma função vazia transmitida como um callback de clique.

Espião Um wrapper sobre um objeto real que também monitora algumas informações adicionais, semelhante a simulações. Elas geralmente são evitadas para aumentar a complexidade. Falsos ou simulações são, portanto, preferíveis a espiões.
Sombra Falsificação usada no Robolectric.

Exemplo de uso de uma instância

Suponha que você queira fazer um teste de unidade de um ViewModel que depende de uma interface chamada UserRepository e exponha o nome do primeiro usuário a uma interface. É possível criar um teste duplo falso implementando a interface e retornando dados conhecidos.

object FakeUserRepository : UserRepository {
    fun getUsers() = listOf(UserAlice, UserBob)
}

val const UserAlice = User("Alice")
val const UserBob = User("Bob")

Esse UserRepository fictício não precisa depender das fontes de dados locais e remotas que a versão de produção usaria. O arquivo reside no conjunto de origem do teste e não é enviado com o app de produção.

Uma dependência falsa pode retornar dados conhecidos sem depender de fontes de dados remotas
Figura 1: uma dependência fictícia em um teste de unidade.

O teste a seguir verifica se o ViewModel expõe corretamente o primeiro nome de usuário à visualização.

@Test
fun viewModelA_loadsUsers_showsFirstUser() {
    // Given a VM using fake data
    val viewModel = ViewModelA(FakeUserRepository) // Kicks off data load on init

    // Verify that the exposed data is correct
    assertEquals(viewModel.firstUserName, UserAlice.name)
}

Substituir o UserRepository por um falso é fácil em um teste de unidade, porque o ViewModel é criado pelo testador. No entanto, pode ser desafiador substituir elementos arbitrários em testes maiores.

Como substituir componentes e injeção de dependência

Quando os testes não têm controle sobre a criação dos sistemas em teste, a substituição de componentes por cópias de teste torna-se mais complexa e exige que a arquitetura do app siga um design testável.

Até mesmo grandes testes completos podem se beneficiar do uso de testes duplos, como um teste de interface instrumentado que navega por um fluxo completo do usuário no app. Nesse caso, convém tornar seu teste hermético. Um teste hermético evita todas as dependências externas, como a busca de dados da Internet. Isso melhora a confiabilidade e o desempenho.

Figura 2: um grande teste que abrange a maior parte do app e falsifica dados remotos.

Você pode projetar seu app para alcançar essa flexibilidade manualmente, mas recomendamos usar um framework de injeção de dependência, como o Hilt, para substituir componentes no momento do teste. Consulte o guia de teste do Hilt.

Robolectric

No Android, você pode usar o framework do Robolectric (link em inglês), que fornece um tipo especial de teste duplo. O Robolectric permite executar testes na estação de trabalho ou no ambiente de integração contínua. Ele usa uma JVM normal, sem um emulador ou dispositivo. Ele simula a inflação de visualizações, o carregamento de recursos e outras partes do framework do Android com cópias de teste chamadas de sombras.

O Robolectric é um simulador. Portanto, não deve substituir testes de unidade simples nem ser usado para fazer testes de compatibilidade. Ele oferece velocidade e reduz o custo às custas de menor fidelidade em alguns casos. Uma boa abordagem para testes de IU é torná-los compatíveis com Robolectric e testes instrumentados e decidir quando executá-los, dependendo da necessidade de testar a funcionalidade ou compatibilidade. Os testes do Espresso e do Compose podem ser executados no Robolectric.