Testes em Android: Test Driven Development com activities e custom views
Introdução
No ínicio do ano passado, estava pareando com um amigo no desenvolvimento de uma feature de um app iOS e acabei propondo uma abordagem para usarmos durante o desenvolvimento dessa feature, logo em seguida fui questionado: "E como vamos testar?" Essa pergunta acabou me marcando muito, principalmente porque me fez perceber rapidamente que eu tinha pouca maturidade com testes, sendo mais preciso, na prática de Test Driven Devevelopment (TDD).
Durante esse último ano (2018), tive a oportunidade de trabalhar com Swift(iOS), Java, React e Kotlin (Android), tentando usar TDD sempre que possível. Desde fevereiro deste ano eu venho trabalhando num projeto android escrito em Kotlin. Trabalhar nesse projeto fez com que meu conhecimento em testes fosse amadurecido rapidamente. O objetivo deste post é falar um pouco sobre TDD e sobre algumas ferramentas que ajudaram no desenvolvimento da minha jornada com TDD.
Test-Driven Development (TDD)
TDD pode ser compreendido como o ciclo de desenvolvimento onde o teste é escrito antes da implementação. O teste vai falhar devido a ausência da feature e os esforços da pessoa desenvolvedora serão redirecionados para fazer com que o teste passe. Após o teste funcionar, a pessoa desenvolvedora terá a segurança de que a feature está funcionando corretamente para a regra aplicada e poderá fazer refatorações no código utilizando o teste que foi escrito para validar se o comportamento anterior foi mantido.
No caso de um teste para uma custom view, seria interessante escrever o teste mesmo que não seja possível executá-lo, seguido pela implementação do XML para viabilizar a compilação do código. Após o teste falhar, a pessoa desenvolvedora estará focada em fazer com que o teste passe. Posteriormente, podem ser realizadas refatorações com o intuito de melhorar o código escrito, repetindo o ciclo inteiro até que o comportamento funcionalidade esteja de acordo com o esperado.
Tudo isso é bem legal, todavia, como executar na prática? Será exemplificado em uma abordagem didática, mas antes serão apresentadas algumas ferramentas e frameworks que podem auxiliar na criação desses testes.
Robolectric
É um framework que possibilita a execução de testes com dependências do sistema operacional android dentro da JVM, em outras palavras, é um simulador do sistema Android. Um teste executado dentro da JVM propicia feedback mais rápido que os executados dentro do emulador, todavia o grau de confiabilidade cai quando comparados com os executados em um emulador ou dispositivo real.
Espresso
É um framework de testes que possibilita a escrita de testes UI para o Android. Ele é composto por três componentes: ViewActions, ViewMatchers e ViewAssertions. É uma ferramenta interessante, pois possibilita que os testes sejam executados em emulador, dispositivo real ou até mesmo em uma device farm. Um dos pontos positivos do framework é que ele possibilita a criação de asserções customizadas, provendo mais legibilidade aos testes escritos.
Android Testing Library
A Google disponibilizou essa biblioteca para auxiliar o desenvolvimento de testes. Antes dela existir, era investido energia na classificação do teste, ou seja, era investido um tempo para definir os frameworks a serem utilizados e se o teste seria instrumentado ou unitário na JVM. A biblioteca cria uma abstração onde o teste escrito pode ser executado tanto na JVM quanto no modo instrumentado, isto é, executa dentro da JVM, emulador ou dispositivo real, e o melhor, utilizando os recursos do espresso. Ela disponibiliza diversas APIs para testar os componentes do android, como: Activities e Fragments
Mockk
É uma biblioteca para fazer mocks em Kotlin. Auxilia a escrita de testes com aspecto mais próximo da linguagem e nas limitações da JVM em relação a coisas como: classes finais e métodos estáticos. A utilização desta biblioteca flexibiliza a escrita dos testes, pois permite abstrair a criação de objetos complexos e disponibiliza recursos de verificações para o objeto gerado pela biblioteca.
Koin
Koin é uma biblioteca para injeção de dependências escrita em kotlin. Ela é simples e possui suporte para utilizar em conjunto com o ViewModel presente na biblioteca de arquitetura de android desenvolvida pela google. A utilização da biblioteca permite que sejam injetadas dependências com comportamentos pré-estabelecidos nos testes, isto é, ela auxilia na injeção de implementações que corrobore o comportamento esperado.
Exemplo
Já foi explicado um pouco sobre as ferramentas que serão utilizadas, que tal exemplificarmos o uso delas dentro de um teste? Vamos criar uma custom view que irá preencher seus campos utilizando um objeto de entrada e deverá executar uma função quando a view for clicada.
Primeiros passos:
Método para adicionar as dependências conjuntas:
O método abaixo deve ser adicionado no arquivo build.gradle do módulo app.
Dependências
Activity de teste
Os testes da custom view que serão criados utilizam a classe ActivityScenario, isto é, uma classe que disponibiliza diversas APIs para iniciar e manipular o estado de uma activity para auxiliar no desenvolvimento de testes. Isso funciona com activities que foram criadas no pacote principal ou que foram declaradas no manifesto de debug, ou seja, o teste necessita de uma activity para que consiga ser executado. Na pasta de debug, crie uma activity e um manifesto, adicione essa activity dentro do manifesto de teste e pronto.
Teste
Os testes são criados no diretório de testes compartilhados, sharedTest, respeitando a hierarquia dos pacotes presentes na aplicação.
No teste acima, foi feito:
1. Iniciada uma activity de teste vazia
2. Instanciada a HeaderView
3. Executado o método activity.setContentView utilizando a custom view
4. Executado o método responsável por popular a view
5. Realizada algumas asserções para verificar se os campos estão com os valores novos.
O teste só será executado após a criação as classes e métodos necessários pra isso. Começamos a criar a custom view, e ela ficou assim:
Após a escrita da View, será necessário criar uma configuração de “Run/Debug Configurations” para executar o teste, isto é, criar configurações para que o teste possa ser executado utilizado o modo AndroidJUnit ou o Instrumentado.
Executamos e o teste falhou, todavia, mas podemos notar a mensagem:
[Robolectric] br.com.tsouto.testsample.widget.HeaderViewTest.setPerson_shouldUpdatePersonInfo: sdk=28; resources=LEGACY
A mensagem acima indica que o teste será executado na JVM utilizando a sdk-28, o teste criado não faz nenhuma referência ao Robolectric (imports/classes), todavia, ele automaticamente utiliza o Robolectric pois está sendo executado dentro da JVM. O teste exibiu um erro de asserção do espresso mesmo tendo sido executado dentro da JVM.
Após criar o teste, é necessário voltar para a implementação do componente, o qual ficou assim:
Após finalizar essa alteração na HeaderView, o teste executado foi aprovado e o comportamento validado sem a necessidade de executar o fluxo dentro de um emulador para chegar na tela em que o componente estivesse acoplado. Foi 'economizado' um tempo de desenvolvimento e o comportamento da HeaderView está sendo coberto por um teste automático.
A próxima alteração do componente consiste em disparar uma função sempre que o botão for clicado. Para fazer isso, foi criado o seguinte teste:
Foi adicionado um método setButtonClick que recebe uma função como parâmetro para ser executada sempre que o botão for clicado, no caso, o teste está enviando um mock. Assim como no teste anterior, será criada a função dentro do componente para garantir que compile e o teste falhe.
Após o feedback da falha, retornamos ao desenvolvimento da função e chegamos na função abaixo:
Logo após a execução do teste, recebe-se o feedback de que o mesmo foi aprovado e o novo comportamento passa a ser coberto por um teste automatizado.
A próxima etapa será criar uma Activity que utilizará uma fonte de dados para preencher os dados na HeaderView. A abordagem escolhida é utilizar um repositório para abstrair as regras de lógica relacionadas a fonte de dados, e por isso, o repositório utilizará contratos para esse fim. Segue a definição do contrato:
Com a definição do contrato se torna possível definir a regra de acesso referente a regra de negocio isoladamente, a implementação será chamada de RemoteDataSource.
O objetivo da implementação acima é simular o acessos a informações externas, como por exemplo, uma API do retrofit ou um banco de dados através de um exemplo de facil entendimento.
A implementação da logica de acesso de dados será injetada no repositório através do construtor.
A finalização da implementação do repositório viabiliza sua utilização pela Activity, que utilizará a sua chamada no onCreate para popular os valores esperados pela HeaderView, no caso, uma instância da data class Person.
O repository está sendo injetado utilizando koin. Portanto é necessário inicializar os módulos na criação do Application.
Legal, o aplicativo foi "finalizado", todavia, não existe nenhum teste para saber se a activity está utilizando o repositório para preencher os dados na HeaderView ou que a HeaderView se encontra disponível no layout da activity.
No teste acima está sendo iniciado a MainActivity, por sinal, muito semelhante ao teste criado para a HeaderView que verifica se a view está apresentando os valores "Mock" e "25 anos". Após a execução do teste, o feedback recebido foi de que o teste falhou, pois os valores presentes na View são os valores definidos pela RemoteDataSource, no caso, "Remote" e "30 anos". É nesse momento que a combinação do koin e mockk consegue viabilizar uma maior facilidade no que se trata de testes de activities, pois o koin possibilita sobrescrita das instâncias definidas previamente. Neste caso, será utilizado para injetar um mock de PersonDataSource. Será adicionado um setUp para a MainActivityTest, segue a implementação abaixo:
O setup está mockando a PersonDataSource e utilizando ela para sobrescrever a implementação que foi definida pela Application. Com a escrita desse setup, o teste foi executado novamente e o mesmo passou e a MainActivity passou a ter um teste que cobre o seu comportamento.
Atenção: Como o objetivo deste é de apresentar a utilização dessas ferramentas no uso de teste de activities e custom views, não foi utilizado nenhuma arquitetura para reduzir a complexidade no entendimento do conteúdo apresentado.
Conclusão
A utilização dessas ferramentas me auxiliaram nos testes de activities, fragments e custom views. Passei a componentizar mais e me senti confiante durante a refatoração de activities. Outro ganho perceptivel foi a economia de tempo durante o desenvolvimento de activities que estavam dentro de outros fluxos, pois eu consegui reduzir a quantidade de vezes que precisava executar a aplicação para validações de alguns comportamentos.