Testes unitários vs aceitação

Rodrigo Sicarelli
Android Dev BR
Published in
7 min readMay 14, 2016

--

Constantemente recebo perguntas como:

Sicarelli, o que devo testar unitariamente ou não, no Android?

ou até

Devo gastar meu tempo fazendo testes de aceitação ou devo apenas focar nos unitários?

Antes de responder essas perguntas e outras que você possa ter, vamos entender melhor o contexto de teste no Android.

Linha do tempo

Todos os testes no Android são baseados no JUnit. De modo geral, um teste em JUnit é um método, anotado, que contém sentenças que testam (ou provam) partes da sua aplicação. Você organiza seus métodos de testes em classes chamadas test cases, ou até mesmo organizar seus testes usando alguns suítes.

No JUnit, você constrói uma ou mais classes e usa algum test runner para executá-los. Esses test runners, são processadores de teste que nos ajudam (lê-se fazem tudo por nós) a instanciar nossos testes em tempo de execução, na execução do próprio teste e a geração de relatórios de resultados. Esses processadores são anotados com o famoso @RunWith no início do seu test case.

@RunWith(AndroidJUnit4.class)
public class SimpleTest {...}

Mas, há muitos anos, todos nós constantemente monitorávamos em qual estado andava os testes no Android, tentando descobrir uma maneira fácil e integrada de se testar. Isso por que, até alguns anos atrás, a única forma suportada de rodar testes era executando-os em alguma Dalvik VM, seja em um device ou emulador.

As ramificações que isso causava fazia seus testes unitários (que devem ser leves e executados rapidamente) demorarem bastante por depender da demora de “subir” um emulador, o que é a única opção quando se fala de um ambiente com CI. Mesmo se você tivesse a audácia de configurar e deixar pronto um AVD, ainda era necessário fazer mais mágica para exportar seus relatórios em XML do JUnit de dentro do emulador.

Um pé no saco, né?

Para nossa sorte, isso era solucionado pelo Roboletric, que basicamente move a execução dos seus testes da Dalvik para uma JVM qualquer, e isso resolve lindamente a sua dependência com algum device/emulador e dos XML’s de resultados, tornando realidade um CI em seu projeto.

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyActivityTest {
private MainActivity activity; @Before
public void setup() {
activity = Robolectric.buildActivity(MainActivity.class)
.create().get();
}

@Test
public void checkActivityNotNull() throws Exception {
assertNotNull(activity);
}
}

Porém, isso era uma pequena dor de cabeça, obrigando te dar a volta ao mundo para retirar isso da teoria e bota-la em prática.

Mas, graças ao time de Android Tools e para a nossa alegria, o Android Studio 1.2 removeu qualquer workaround ou hack para utilizar o Roboletric. Essa solução foi introduzida na versão 1.1, que tinha uma flag experimental para habilitar testes unitários, e na versão 1.2 essa flag não era mais experimental.

Como funciona?

Basicamente, o plugin do Gradle irá compilar todo código fonte encontrado no caminho src/test/java e irá executá-lo usando os mecanismos de teste do próprio Gradle. Em tempo de execução, os testes serão executados encima de uma versão modificada (reduzida) do android.jar, onde todos os modificadores finais foram retirados, o que significa a possibilidade de usar bibliotecas de mocking, como nosso querido Mockito.

@Test
public void test1() {
MyClass test = Mockito.mock(MyClass.class);
when(test.getSomeValue()).thenReturn(42);
assertEquals(test.getSomeValue(), 42);
}

Tipos de teste

Tudo bem, entendemos qual foi a trajetória dos testes no Android, mas a pergunta ainda continua: o que devo testar unitariamente?

Teste unitário

Um teste unitário testa uma pequena unidade de uma funcionalidade, geralmente um método ou uma função (dado uma classe com um estado particular, chamando o método X dessa classe, Y deve acontecer).

Eles devem ser focados em uma funcionalidade em particular (quando chamar o método de popular informações na tela, e os dados estiverem vazios, deve ser lançado alguma exceção).

Idealmente, todos os testes devem ser executados na memória, o que significa que seu teste e o código que você está testando não devem realizar:

  • Chamadas externas para colaboradores (bibliotecas)
  • Acesso a rede
  • Bater em algum banco de dados
  • Usar arquivos de sistemas
  • Rodar uma thread
  • etc.

Qualquer tipo de dependência que deixe devagar/difícil de entender/ inicializa/manipula seus testes deve estar devidamente tratado através de stubs e mocks utilizando as técnicas apropriadas para que você foque no que a unidade do seu código está fazendo, não no que sua unidade está dependendo.

Em suma, os testes unitários são os mais simples possíveis, faceis de debuggar, de confiança (devido a fatores externos reduzidos), rápido para executar e ajuda a provar que mesmo a menor parte do seu código/funcionalidade está funcionando como planejado antes que sejam integradas. A grande questão é que, embora você possa provar que eles funcionam perfeitamente isoladas, as unidades de código podem “explodir” quando combinados, o que nos leva a…

Teste de integração

Os testes de integração tem como foco juntar/combinar as unidades do seu código e testar se o resultado dessa combinação funciona corretamente, seja em um só sistema ou a combinação de vários outros. Além disso, outra coisa que diferencia os testes de integração de testes unitários é o ambiente. Testes de integração podem e irão usar threads, acessar o banco de dados ou fazer o que for necessário para garantir que todo o código e as diferentes alterações de ambiente irão funcionar corretamente.

A principal vantagem é que eles irão encontrar bugs que os testes unitários não foram capazes de encontrar. Porém, a principal desvantagem é que os testes de integração “tocam” em mais código, são menos confiáveis, os testes falhados são mais difíceis de se diagnosticar e os testes são mais difíceis de manter.

Também, testes de integração não necessariamente prova que sua funcionalidade funciona por completo. Talvez o usuário não se importe com os detalhes internos da sua aplicação, o que nos leva a…

Testes funcionais

Testes funcionais verificam uma característica particular comparando com os resultados para uma determinada entrada contra a especificação (esperado). Testes funcionais não se preocupam com os resultados intermediários ou efeitos colaterais, apenas o resultado final (eles não se importam que depois de fazer x, y objeto tem estado z). Eles são escritos para testar parte da especificação, tais como, “chamando a função calcula(x) com o argumento 2, retorne 4”. Mas, em nosso mundo, testar se a funcionalidade funciona dessa maneira não garante que a experiência do usuário dentro do sistema está garantida, o que nos leva a…

Testes de aceitação

Um teste de aceitação padrão envolve a execução de testes em todo o sistema para garantir se a funcionalidade da aplicação satisfaz a especificação. Por exemplo. “Clicando no botão de login, deverá mostrar um progresso na tela sumir quando o login for completado”. Não há continuidade real dos resultados, basta uma aprovação ou reprovação resultado.

A vantagem é que os testes são escritos em uma “língua” ou contexto (dado situação X deve acontecer Y) e garante que o software, como um todo, tem suas funcionalidades feitas e completas. A desvantagem é (como talvez vocês já tenham percebido) que, conforme seu teste move de uma camada para outra, "toca" em mais código e rastrear um erro no meio disso tudo pode ser muito difícil.

Por exemplo, uma funcionalidade falha em 1% das vezes. Achar o motivo que causa esse 1% de erro pode ser muito difícil (ou até impossível) o que nos força a gastar horas e horas investigando o problema.

Um conjunto de testes de aceitação é basicamente uma especificação executável escrito em uma linguagem específica que descreve os testes na linguagem utilizada pelos usuários do sistema.

No caso do Android, um teste de aceitação requer o próprio Android, ou seja: você precisa de um device para provar que seu aplicativo está funcionando como esperado. Isso implica algo que comentei no início do post: seus testes irão demorar mais por toda essa dependência que você vai ter.

Mas, qual devo utilizar?

Aqui está a parte delicada.

Por um lado, temos o teste unitário: rápido, dedicado a pequenas partes do seu código que garantem a funcionalidade individual dos seus componentes, possibilitando de maneira fácil e rápida identificar um bug específico, te dizendo o comportamento esperado e mostrando qual foi a falha. Porém, requer toneladas de testes e outra tonelada de mocks (sabendo que é difícil você não ter dependência do Android em algum método. Um mock do context já vira o DNA do seu teste) e, no caso de aplicativos para celular, temos casos de uso muito específicos que precisam garantir uma experiência agradável para todos os usuários de todos os telefones.

Por outro, temos os testes de aceitação, que junto com o Espresso, garantem que sua funcionalidade será aprovada pelo usuário. Você irá cobrir muito mais partes do seu sistema evitando crashs específicos de plataforma e não precisando se preocupar em mockar muita coisa (tendo em vista que roda encima de algum device/emulador). Mas, quando acontece um erro, vai ficar muito difícil responder a pergunta: por que esse erro aconteceu?

Conclusão

Idealmente, quanto menos regras de negócio seu aplicativo ter (unitários), mais tempo você irá ter para testar casos de uso e garantir uma experiência limpa e fluida no seu aplicativo (aceitação).

Pense em um cenário onde você tenha diversos clientes (Android, iOS ou até Windows Phone) e sua regra de negocio não esteja centralizada.

Por exemplo: a exibição da data para o usuário final deve ser a mesma na plataforma, e cada plataforma vai ter um jeito diferente de tratar essa data, criando código de formatação e consequentemente criando testes unitários para garantir que independente do formato que a data vir do servidor, o sistema irá conseguir formatar de acordo com a requisição. Percebe que você pode evitar isso já retornando a data formatada do servidor que, por sua vez ,já tem um (e em só um lugar) teste unitário criado para garantir isso?

Se falarmos das vantagens que temos possuindo as regras de negócios centralizadas em um só lugar, daria um belíssimo e extenso post. Mas, só com esse exemplo você provavelmente conseguiu entender que, em nossa realidade, é muito mais vantajoso criarmos mais testes de aceitação do que unitários, o que significa que nosso código está com poucas regras de negócios, assim podemos focar mais em entregar uma aplicação solida em qualquer device.

Então, mão na massa e bora testar!

--

--