Na Invillia, toda quarta-feira, ao meio dia, paramos uma hora para nos nutrir com as dicas, how-tos, boas práticas e tendências selecionadas por nossos especialistas em Product, Agile, Back e Front, Mobile, Quality, Security e Data. Uma troca de experiência vital para quem adora o novo. E essencial para que a inovação nunca pare. Se a tecnologia está no sangue, a gente faz questão de deixá-la circulando cada vez mais_
NA VEIA_ Testes Instrumentados no Android
11 minutos de leitura
No artigo de hoje, compartilhamos os principais aprendizados da incrível edição sobre “Testes Instrumentados no Android”, apresentada por Fabiano Gomes de Mel Junior, nosso especialista no assunto.
Introdução
Não dá para falar de testes instrumentados sem mencionar os testes de modo geral porque a parte mais interessante e relevante é saber o que testar e qual segmento será testado, na perspectiva do desenvolvedor. Assim, as referências utilizadas para falar do tema aqui são a documentação do Android (abordagem e testes) e acadêmicos da engenharia de software como Marco Túlio Valente, Kent Beck, Martin Fowler e Dave Farley.
Dentre os principais benefícios de testes estão a possibilidade de verificar corretude, comportamento funcional e checagem de usabilidade.
Considerando que o teste não indica a ausência de erros, mas a presença deles, pode-se pressupor que, no cenário testado, há uma visão correta daquele comportamento de maneira funcional, o que não significa que o sistema esteja livre de “bugs” – porque sempre existem casos e formas diferentes de interação que podem acabar não sendo testados ou verificados em produção. Por isso, o que traz segurança em alguns fluxos é o que já foi testado e verificado, além da ideia do que não investigar quando se está com um problema.
Além disso, a questão da checagem de usabilidade serve mais para o instrumentado ver como está a experiência do usuário (a exemplo do time da Invillia que estava desenvolvendo uma das bordas com um botão de “resgatar” pequeno de um produto – design focado –, tornando um pouco complexo o seu uso. Ao realizar este tipo de teste manual, o time já percebeu que não era funcional e trouxe este ponto de melhoria).
Tipos de testes
Os testes relativos à execução estão divididos entre dois principais:
- Testes manuais, cujas características tem-se:
- Custo em execução – executados manualmente por uma pessoa no aplicativo e sem custo de tempo em desenvolvimento;
- Baixa escalabilidade porque, a cada teste de fluxo, uma pessoa novamente será alocada para esta tarefa; e
- Resultado variável, ou seja, mesmo que haja um roteiro, por exemplo, para testar o fluxo, pode ser que uma pessoa execute uma tarefa de maneira diferente, ou ainda que siga sempre o mesmo modelo num roteiro mais próximo do ideal e nunca consiga identificar um “bug”. Por isso, pode ser variável o acaso que indica um resultado de sucesso ou detecte um novo “bug”.
- Testes automatizados, que podem ser um pouco diferentes:
- Custo em desenvolvimento, pois precisará de alguém para programar o teste e colocá-lo para rodar desde o início do desenvolvimento do software;
- Alta escalabilidade, porque se trata de um teste automático, ou seja, com efetividade e eficiência das máquinas, sem a necessidade de alocar alguém;
- Resultado absoluto, quando há a possibilidade de algum problema que tenha sido projetado – e para que ele não dependa de nada externo – tende a sempre dar o mesmo resultado, independente de onde você execute ou empregue configurações. Isso porque, num cenário ideal, teremos um resultado absoluto que não varia muito;
- Sujeito a intermitência.
Diferenciação dos testes em relação ao objetivo
- Os testes funcionais verificam se a aplicação está se comportando da forma como deveria, colocando em diferentes sistemas para verificar a questão do teste de compatibilidade, por exemplo, quando uma página web se comporta de maneira ideal em diferentes versões de navegadores;
- Os testes de performance verificam se o aplicativo é rápido e eficiente; e
- Os testes de acessibilidade avaliam o que está em alta para tentar garantir uma experiência agradável para todos os usuários, se preocupando se o produto está acessível para uma gama variável de clientes e usuários.
Tipos de Escopo
- Testes unitários: verificam uma porção mínima do app, métodos e classes, voltados para as regras de negócio no domínio da aplicação;
- Fim a fim: ou seja, de ponta a ponta, que verificam grandes porções, como uma tela complexa (por exemplo, um formulário com validação de vários campos, implementação de regras e exibição de campos) ou fluxo inteiro (como num cadastro onde se tem uma tela de on boarding com introdução sobre o produto, e depois há a necessidade de informar seu nome e como gostaria de ser chamado, inserindo ainda e-mail e senha válidos). Por se tratar de vários passos, é interessante que seja executado um teste de fim a fim que acaba levando mais tempo e mais interações;
- Testes médios/de integração: que ficam no meio termo, ou seja, são grandes demais para serem um teste unitário mas, também, pequenos e não complexos o bastante para serem testes de fim a fim, pois realizam a integração entre unidades, como, por exemplo, um método de login, ou seja, algo mais simples que difere de um cadastro. (No login, se tem a entrada dos dados do usuário e, logo em seguida, uma validação, finalizando com uma verificação do direcionamento do usuário).
Diferenças entre testes locais e testes instrumentados
- Testes instrumentados
Via de regra, eles rodam em device android, ou seja, possuem interação com frameworks, sendo necessário que o app seja “buildado”, gerando, inclusive, um aplicativo que será instalado no simulador e, ao clicar, realizará o passo a passo no teste. Além disso, geralmente são utilizados testes instrumentados para testes de UI com interação do usuário, simulando teste manual, só que de forma automática.
- Testes locais
Costumam rodar em uma máquina de desenvolvimento ou no servidor, sem a necessidade de ser um device android. Além disso, geralmente são pequenos e rápidos por conta de serem testes unitários, e isolam o objeto do teste do restante da aplicação, o que significa que não será necessário abrir um aplicativo completo para fazer um teste unitário, mas necessita, geralmente, das classes para rodar.
Exceções:
Em testes locais grandes, por exemplo, para testar coisas do android ou framework localmente utilizando uma das principais bibliotecas como Roboeletric, que faz o carregamento das classes bases e, a partir disso, realiza testes sobre o comportamento de um fragment com tratamento específico, acaba tendo utilidade para checar, por exemplo, o comportamento de um listener numa listagem de objeto. Isso é interessante para não tirar a validação na lógica dos testes instrumentados até por questão de economia de tempo e complexidade;
Por outro lado, em testes instrumentados pequenos, verifica-se a integração com código e o framework como, por exemplo, o SQLite – sistema de banco de dados que consome abstração – e é bastante interessante para validar a lógica da interface realizando um teste pequeno e simples para ver se a senha está integrando e criando a tabela com registro para exclusão posterior.
Estratégia para testes
Cobertura – o quanto o código estará coberto através de diferentes métricas – tais como linhas de código, função etc. – ou seja, como cobrir 100% da aplicação com tudo testado? Não é uma boa ideia por economia de recursos, pois quando se trabalha com algumas linguagens, há “gaps” que possuem alguma lógica e são interessantes para testes, mas às vezes só retornam o valor de uma variável privada e que pode não fazer muito sentido;
Fidelidade, velocidade e confiança – que, normalmente, andam juntos, mas são antagônicos em velocidade – ou seja, como balancear? Porque se o teste for o mais preciso possível, o processo é lento além de exigir entrega, integração com tempo de resposta etc. que não é interessante, porque a ideia é que os testes tenham frequência, daí, normalmente, é necessário retirar um pouco da fidelidade e, consequentemente, diminuir um pouco da confiança do teste para trabalhar com um cenário mais controlado e ganhar bastante na velocidade num cenário mais rápido. Por isso, geralmente a estratégia comum para dar uma compensada é colocar cada vez mais cenários que cobrem mais as possibilidades, que apesar de perder na confiança e fidelidade de um teste específico, acaba abrangendo cada vez mais os cenários testados;
Arquitetura testável – importante logo na hora do desenvolvimento e aplicação que envolve, em primeiro lugar, o desacoplamento, evidenciando, por exemplo, o princípio de inversão da dependência, ou seja, visa diminuir ao máximo o acoplamento para tentar trabalhar mais com coesão; o princípio de responsabilidade única, pois quando há funções que realizam diversas coisas, é necessário muitos cenários de teste para validar todas as possibilidades, mas com responsabilidade unificada, para testar determinada função, serão necessários no máximo dois testes (o caso ideal, que representa e o que se espera que a função realmente execute e talvez um caso mais comum), podendo aumentar em virtude do que esperar que não funcione adequadamente, o que exige tratamento de como ela está respondendo (quanto menor a quantidade, mais fácil e simples enxergar o cenário que precisa testar); a lógica de negócio separada da lógica de apresentação, que corresponde à separação das camadas, o que possibilita realizar todos os testes na lógica de negócios de forma unitária, ou seja, uma lógica de apresentação estará vinculada em outras tecnologias, tornando muito mais fáceis os testes instrumentados de maneira separada;
Testes intermitentes – como evitar os testes que hora passam e hora não passam?
Princípios para testes
Quando se tem visão de como se deseja construir a aplicação, é necessário possuir alguns princípios para realizar esses testes, que se aplicam tanto a teste unitário quanto instrumentado:
- Eles precisam ser rápidos, ou seja, não podem demorar, por exemplo, 3 ou 5 segundos para execução (considerando a quantidade), visando ter escalabilidade e frequência – quanto mais rápido geralmente melhor;
- Têm que ser independentes, ou seja, um teste não pode depender do resultado de outro, visando confiança, por exemplo, se possui uma cadeia de testes em que o primeiro falha e implica em outros 10 pararem, por alguma intermitência, resultarão em 11 testes falhando, o que leva a uma impressão imediata de que está acontecendo um grande problema, só que, na verdade, tem um teste falhando a lógica dos próximos que pode estar completamente perfeita, ou seja, o resultado dos testes de fato representam aquilo que está “quebrado”;
- Devem ser determinísticos, ou seja, intermitentes, que variam o resultado de acordo com a execução em virtude de inúmeros motivos, por exemplo, um número de séries ou cenários que este teste falharia, pois quando um servidor não está disponível, isso não deveria impactar o teste sobre a saúde da aplicação;
- Precisam ser auto verificáveis, ou seja, deve ser fácil entender se o teste deu um resultado certo ou quebrou e, também, por quê quebrou, a exemplo de ter um determinado teste que faz contas e que, no final, deveria retornar um número, mas falhou;
- Eles devem ser escritos o quanto antes, e o principal motivo é a simplicidade, a exemplo da técnica para desenvolvimento que se baseia em escrever o teste primeiro e, logo em seguida, fazer as implementações se adequarem aos testes, o que pode ser muito interessante porque acaba trazendo o pensamento de criar um template que será executado e, em seguida, fazer a sua implementação no teste, pois se for necessário fazer alguma especificação é necessário sobrescrever o que varia entre eles. Além disso, já se possui um começo de desenvolvimento do que quer fazer, como fazer e o que deve retornar. Uma vez tendo isso claro, é fácil implementar e pensar nos casos de exceção, considerando o enviesamento quando se escreve a aplicação de como ela deveria se comportar quando se planeja em casos diferentes.
Test Smells
Sabendo os princípios de estratégia, os test smells dizem respeito às práticas que se deveria ter em mente quando está escrevendo um teste:
- Teste obscuro – não é interessante ser longo demais e complexo, pois o teste deve ser o mais simples possível para que alguém “bata o olho” e entenda o que está acontecendo e porquê;
- Teste com condicional – são lineares porque podem acabar se tornando um teste intermitente, por exemplo, quando se coloca uma operação que só está disponível uma vez por ano e é necessário checar a sua data, não sendo interessante por conta da necessidade de quebrar este teste com condicional em dois testes para validação (true e false);
- Código duplicado – que não é absoluto e deve evitar se repetir conforme for necessário, mas às vezes precisa dar uma duplicada no código;
- Múltiplos asserções – que são as verificações em destaque por não serem algo absoluto, mas, geralmente, uma boa prática com única asserção pode garantir aquele escopo do teste de responsabilidade única. No entanto, quando se está realizando teste de integração, como, por exemplo, fluxo de login, e coloca uma senha errada primeiro e depois uma certa, pode ser interessante ter múltiplo asserts num único teste, principalmente, testes de ponta a ponta.
Como definir o que testar?
Há alguns testes unitários essenciais:
- ViewModel, Presenters – baseados na regra de negócios;
- Independente de plataforma – camada de dados: repositórios; camada de domínio: casos de uso (Teste double);
- Utils.
Além disso, é interessante sempre testar Edge Cases na parte de:
- Utilidade de cenários que não deviam acontecer;
- Rede: erros na chamada, json incorreto;
- Camada de dados, que diz respeito a salvar um arquivo quando o armazenamento está cheio; e
- Questões do ciclo de vida da aplicação, que recria um objeto no meio dele, rotacionando o dispositivo, por exemplo, em teste instrumentado.
Portanto, o que se deve evitar?
- Corretude de biblioteca de terceiros, que significa testar algo que já foi testado por quem disponibiliza uma ferramenta popular, o que acaba resultando em trabalho de baixo valor, pois se já tiver testado toda a aplicação com tempo sobrando, tudo bem, mas quando é necessário fazer outros testes, é melhor deixar a questão de biblioteca de terceiros para depois;
- Activity, fragment e services, que podem ser estendidos para outras bibliotecas. Não é tão interessante porque não têm lógica de negócio, ou casos padrões que vão impactar diretamente no seu funcionamento.
Testes de UI
Tudo o que foi falado sobre teste unitário não deve ser um teste de UI, pois o teste de UI é o teste instrumentado que foca, principalmente:
- No teste de tela – com escopo menor – que trata a interação com uma única tela e pode até ser um teste de integração para testar a tela e ver se a navegação está correta;
- E o teste de fluxo – que será mais que isso – que ocorre com o teste de tela durante a navegação. Simplesmente abre após uma tela disparando um evento. No contexto do android, por exemplo, em teste de fluxo, quando o usuário vai testando um fluxo inteiro, e retorna à questão dos testes de cadastro que passa por várias fases e, no final, conclui um objetivo que é criar este registro do usuário.
Duplicatas de testes
São utilizadas seis categorias de duplicatas para testar a lógica da interface:
- Fake – possui implementação funcional conveniente, onde é simulado um banco de dados em memória para validações na hora de salvar um usuário como, por exemplo, para testar um caso de uso, é necessário um repositório para comunicar com o banco de dados, a fim de passar essa integração do repositório e checar a lógica no caso de uso – como está tratando o usuário – direcionando o seu médico para fazer armazenamento para recuperar depois, e é conveniente porque roda de forma simples e prática;
- Mock – é basicamente uma interface onde é possível sugerir como ela deve se comportar, ou seja, dado determinado método, pode retornar uma falsa checagem, por exemplo. Há inúmeras bibliotecas para isso, pois é bastante utilizado no dia a dia;
- Stub – funciona para replicar um comportamento de forma funcional como, por exemplo, ao testar uma integração de terceiro, é necessário criar um API que retorna ao Windows no mesmo formato que o API de terceiro retornaria. O problema é que se prefere utilizar o Fake ao Stub porque é mais simples e não requer aplicação que consuma API para comunicar com ela. Entretanto, existem formas e momentos que podem tornar interessante executar tudo isso como, geralmente, em testes menores em quantidade, para ver se os contratos estão sendo respeitados como integração de terceiros, ou seja, quando duas empresas estão trabalhando em parceria com uma empresa desenvolvendo front e outra desenvolvendo back, e o pessoal manda um contrato para desenvolver um Stub para ver se a integração está funcionando;
- Dummy – objeto passado adiante, mas não utilizado no teste em si, ou seja, funciona mais como parâmetro necessário e não para questão de monitoria de métodos, pois é um argumento obrigatório ao que está sendo testado, mas não tem interação e será passado a frente;
- Spy – o nome já diz “espião”, e este segue o teste como um tracking de interações, estados etc., sendo considerado invólucro para seu objeto. Ele não demonstra ser muito interessante porque aumenta muito a complexidade do teste que exige introduzir uma classe, fazer a checagem nesse estado, um registro dele conforme o teste vai passando, sendo possível atingir estes mesmos objetivos com Fakes e Mocks, além de identificar bugs através do android ou outra linguagem no momento do estado do objeto em si;
- Shadow – é basicamente um Fake utilizado no Roboeletric focado em “fakear” as questões mais relativas ao framework android diante de uma activity etc.
Por que escrever um teste instrumentado?
E qual o fluxo de login de um usuário nesse aplicativo?
O cenário é simples em um aplicativo genérico cujo processo tem início na idealização, onde o usuário seleciona o campo de e-mail e, em seguida, insere o e-mail, selecionando o campo de senha e inserindo uma senha, navegando para a próxima tela se o processo de login foi feito adequadamente. Para fazer esse tipo de teste instrumentado de tela, é necessário utilizar uma biblioteca do android e o código. Em seguida, para a execução é necessário fazer a seleção da view (ID) colocando o “matcher” que será responsável por encontrar o que está sendo procurado e, em seguida, digitar o e-mail do usuário para que esta referência da view selecione um performer para executar uma ação de clique. Todo este processo não é muito agradável aos olhos e, por isso há alguns patterns para a escrita de testes.
GWT:
- Given – quando uma ação específica acontece, espera-se um resultado específico que é bastante utilizado para testes unitários em geral e, também, a forma como se escreve orientado ao comportamento (que começa um pouco antes);
- When – quando a ação em si será executada;
- Then – resultado esperado.
Código seguindo o padrão GWT Pattern: dado determinado cenário, tem a ação de checagem, que se não for possível executar, vai retornar para a parte de testes no mercado.
Em teste instrumentado, tem-se o padrão de Robot, cujo princípio é abstrair a lógica de interação com os componentes do teste para facilitar a leitura. Por isso, para a implementação, é criado um robot base com métodos comuns e interessantes para cada tela (com robots específicos e interação que fuja desse padrão). Por fim, sua escrita é bastante simples, pois se baseia no pattern build, que significa “construir o passo a passo”.
A escrita do Robot Pattern fica mais ou menos assim: primeiro é criado um robot de login, submetendo e checando erro nos campos de usuário e senha para garantir que está funcionando. Com um usuário válido e sua senha, é necessário verificar novamente e garantir que ele abra o aplicativo.
Tudo isso fica muito mais simples do que se comparar com outras formas de execução, pois ao “bater os olhos” num teste robot, é possível identificar o que está acontecendo sem precisar se preocupar com a escrita, o que auxilia no objetivo do teste, que também serve como documentação. Ao olhar o teste, se entende como aquele curso deve funcionar e o que ele suporta.
Por fim, a parte principal de teste é saber o que testar, como testar e qual o cenário de cada teste, por isso é importante saber o que é um teste unitário, seu escopo, o que é um teste instrumentado, e como fazer para seguir a pirâmide de testes tentando deixar mais testes unitários do que testes instrumentados.