Testes de mutação no Android. Valem a pena?
Resultado de um estudo realizado na Natura&CO sobre testes de mutação no Android com Kotlin em 2020
Como consultor de desenvolvimento Android na ThoughtWorks, tenho tido o enorme privilégio de acompanhar a transformação digital da Natura&CO de perto. Uma transformação contínua e necessária para que a Natura se diferencie cada vez mais no mercado e também consiga escalar o desenvolvimento dos seus produtos digitais, inclusive do Super App que temos em produção e que atende a demanda de milhares de clientes. O interessante desse tipo de transformação organizacional é que quem a aplica tende a ganhar melhores orientações a boas práticas de desenvolvimento de software, melhores processos de entrega e por consequência uma maior qualidade no produto desenvolvido.
Os reflexos dessa transformação chegaram no time no qual eu trabalho, a tribo financeira da Natura. Aqui, tivemos a chance de conduzir uma investigação sobre um tipo de teste automatizado que acredito ser muito interessante e ao mesmo tempo desconhecido por muitas pessoas do mundo Android. Esses são os testes de mutação. Nosso objetivo era entender se esses testes poderiam entrar no nosso processo de entrega como uma ferramenta que aumentasse a qualidade do nosso produto.
No caso específico desse artigo, vamos falar sobre testes de mutação no Android com Kotlin.
Por definição, esse tipo de teste visa responder uma única, complexa e abrangente pergunta:
Os testes automatizados que você está criando no seu projeto estão realmente bem implementados?
Os testes de mutação têm como objetivo garantir a qualidade dos testes escritos. Seu código de produção vai ser testado diretamente pelas ferramentas tradicionais do Android (JUnit, Espresso, Robolectric, etc…) e indiretamente pelas ferramentas de testes de mutação.
Nesse artigo vamos compartilhar os resultados dessa investigação!
Como funcionam os testes de mutação?
Para garantir que seus testes estejam bem implementados, um teste de mutação vai aplicar uma modificação aleatória em um ponto específico do seu código de produção e rodar os testes que cobrem esse ponto. Essa alteração gerada automaticamente pela ferramenta é chamada de mutação.
Após aplicada a mutação, os testes automatizados serão executados em cima do código original e também em cima do código alterado.
Considerando que todos os testes executados sobre o código original passem, quando a ferramenta executar os mesmos testes sobre o código com mutação, alguns cenários podem acontecer:
- Os testes executados em cima do código com mutação podem falhar, indicando que o teste realmente falha para alterações não desejadas, o que leva à conclusão de que a mutação foi capturada e seu teste está bem implementado 👍 ✅
- Os testes executados em cima do código com mutação podem passar, indicando que seu teste está mal implementado pois mesmo com uma alteração aleatória no seu código seu teste não identificou nada 👎 🔴
- Nenhum teste cobriu a área com mutação, indicando que seus testes não estão cobrindo uma área passível de defeito 👎 🔴
Percebam que quando rodamos um teste sobre um código com mutação, falhar é um sinal positivo. Isso significa que possíveis bugs naquela área podem ser identificados e seus testes estão garantindo a qualidade do código.
Um ponto interessante que vale mencionar é que nenhum código de teste precisa ser escrito para aplicar essa análise. O que precisamos fazer é configurar a ferramenta de forma correta e ela fará uso de tudo que já existe no projeto para você.
Como resultado, a ferramenta de testes de mutação também pode gerar um relatório descrevendo quantas mutações foram tratadas e quantas não foram:
Para finalizar a explicação, gostaria de compartilhar uma frase que gosto muito e que para mim gera uma reflexão interessante sobre a escrita de testes:
“Never trust a test you haven’t personally seen fail”
(Nunca confie em um teste que você nunca viu falhar)
— Autoria Desconhecida
Que ferramenta podemos utilizar para aplicar testes de mutação no Android?
Para aplicar testes de mutação no mundo Java e da JVM (Java Virtual Machine), nós temos algumas opções disponíveis. Muitas delas, pouco conhecidas e sem exemplos de aplicação real com produtos em produção.
Dentre as ferramentas pesquisadas nesse estudo de caso, apenas uma se destacou como possível candidata para entrar em um projeto Android de larga escala. Essa ferramenta é a Pitest. Além de ser a ferramenta com melhor documentação e facilidade para configuração, ela também foi a melhor classificada nos benchmarks pesquisados. Por isso, para nosso estudo de caso nós a escolhemos como ferramenta a ser explorada.
Para ilustração, segue abaixo um exemplo com comparações entre várias ferramentas de teste de mutação para Java, incluindo a Pit(est):
Desafios encontrados com Pitest no Android
O Pitest é uma ferramenta considerada o estado da arte para testes de mutação com Java e com a JVM (como é dito na página principal da ferramenta). Porém o grande desafio que temos é: os aplicativos modernos estão deixando de utilizar Java, e o Android não roda na JVM.
Como sabemos, o Kotlin é a linguagem oficial recomendada pelo Google para novos projetos Android. Além disso, o Android tem a sua própria virtual machine, que seria uma versão mais leve da JVM com bytecode no formato DEX.
Ao tentar aplicar o Pitest no Android, logo nos deparamos com as consequências desse desafio e limitações que outras pessoas também passaram:
Como o Pitest é open-source e está em constante atualização, logo os criadores e a própria comunidade trataram de ajudar a resolver esses problemas criando plugins específicos para cada situação. Um para resolver o problema do Android e outro para resolver o problema do Kotlin. Infelizmente nenhuma solução foi desenvolvida para atender especificamente as necessidades de projetos Android com Kotlin 😢
Mesmo com as limitações apresentadas, a prova de conceito continuou para ver o acontecia caso aplicássemos o plugin Android em um projeto de larga escala feito em Kotlin.
Resultados da aplicação de testes de mutação em um projeto Android com Kotlin utilizando Pitest
O escopo abordado é de um projeto totalmente em Kotlin fortemente baseado nas funcionalidades que a linguagem tem a oferecer. Do nosso lado, sempre tentamos escrever um código idiomático e fortemente baseado em Coroutines.
Como o Pitest no seu estado atual se comportou nesse escopo?
Spoiler: infelizmente a resposta do plugin Pitest Android não foi satisfatória para o contexto de um projeto Android com Kotlin. Por isso, desencorajamos a prática de testes de mutação em projetos Android de larga escala com as ferramentas que temos disponíveis hoje (Jun/2020).
O Pitest não funcionou muito bem ao aplicar mutações considerando algumas palavras-chave do Kotlin. Uma delas é o when que acabou gerando o seguinte erro:
The class com.example.demo.TestClass$WhenMappings does not contain a source debug information.All classes must be compiled with source and line number debug information
Também tivemos problemas com algumas outras classes que utilizavam anotações como @Parcelize no código, gerando um erro similar.
The class com.example.demo.Model$Creator does not contain a source debug information.All classes must be compiled with source and line number debug information
No caso das Coroutines, está explícito que o plugin específico do Kotlin ainda não tem suporte para essa funcionalidade.
Existem artigos de pessoas com problemas similares ao tentar aplicar testes de mutação em projetos Android com Kotlin. Uma alternativa para contornar o problema seria ignorar algumas classes específicas e só considerar classes mais simples ao aplicar as mutações. Ao fazer esse ajuste a ferramenta funcionou corretamente em alguns casos.
Dado o resultado que tivemos, nos deparamos com o seguinte dilema: deveríamos adotar uma ferramenta nova na nossa pipeline de delivery para aplicar testes de mutação em classes simples que não usam muitos recursos do Kotlin? Ou seria melhor simplesmente não utilizar testes de mutação dado o esforço que teríamos para manter a ferramenta?
Devido a essas limitações encontradas, concluímos que o esforço de manter uma ferramenta nova seria mais alto que o valor trazido por ela. Por isso, desencorajamos incluir uma ferramenta de teste de mutação em um projeto Android de larga escala com Kotlin no momento.
Então, já que não temos testes de mutação, como garantir que nossos testes estão bem implementados?
Garantir que os testes estão bem implementados é um tarefa difícil que exige disciplina. Porém algumas ações podem ajudar que o caminho seja menos doloroso. Para isso você pode:
- Potencializar boas práticas nos testes unitários em Java e Kotlin.
- Implementar uma boa estratégia de pirâmide de testes que possa ser seguida fielmente pelo seu time.
- Favorecer os testes que testam comportamento e não implementação. Para isso recomendo a leitura do livro de testes unitários escrito pelo Vladimir Khorikov.
- Ser uma pessoa próxima a métricas de qualidade.
- Reforçar a prática do TDD. Caso não consiga aplicar o TDD no seu time, pelo menos faça o teste falhar uma vez antes de considerá-lo pronto.
Referências utilizadas
- http://thatsabug.com/blog/gentle_intro_mutation_testing/
- https://blog.activelylazy.co.uk/2017/01/24/never-trust-a-passing-test/
- https://medium.com/@s4n1ty/experimenting-with-mutation-testing-and-kotlin-b515d77e85b5
- https://blog.frankel.ch/experimental-kotlin-mutation-testing/
- https://pitest.org/
- https://arxiv.org/pdf/1707.09038.pdf
- https://blog.trifork.com/2016/09/07/adequacy-of-android-unit-tests/
Esperamos que esse artigo tenha contribuído em algo na sua jornada técnica. Qualquer dúvida ou feedback pode escrever nos comentários abaixo! Obrigado!
Agradecimentos a Poliana Florencio, Lucas Valadão, Lucas de Souza da Conceição e Eduardodribas pela revisão!