Category Archives: software

Serão os testes unitários meros placebos?

Vou começar por um definição simplificada de testes unitários:

Os testes unitários são pedaços de código que testam outros pedaços de códigos.

Supostamente, para se chamarem unitários, os pedaços de códigos a testar têm que ser pequenas unidades, mas como essa definição é ambígua, digamos que esses pedaços de código testam funções. Uma função tem inputs e outputs e os testes unitários verificam que para certos inputs obtenho sempre certos outputs. A parte do “sempre” é a mais interessante, pois abre caminho para a automatização desses testes. Posso ter uma ferramenta que corre todos os testes unitários sempre que eu quiser: de cada vez que compilo, de cada vez que faço commit das alterações no sistema de controlo de versões, de cada vez que gero um executável para o cliente, etc. Se algum teste falhar, serei avisado por essa ferramenta, e corrigirei um potencial bug em produção. Quando todos os testes passarem, terei mais confiança no código que acabei de desenvolver.

Mas ter confiança naquilo que faço não é o objectivo do desenvolvimento de software, quando muito será um (desejável) efeito secundário. O objectivo é que a aplicação resolva um dado problema sem erros.

Antes de continuar, gostava de partilhar convosco que desenvolvi a minha primeira bateria de testes unitários há 10 anos, quando o Kent Beck e o Erich Gamma lançaram o JUnit, uma das primeiras bibliotecas (talvez a primeira) criadas para auxiliar o desenvolvimento destes testes. Na altura, desenvolver baterias de testes unitários que corriam automaticamente quando se fazia uma versão da aplicação era um esoterismo. Entretanto, tornou-se prática mais comum e muitas das frameworks mais recentes trazem incluídas de raiz suporte para testes unitários, funcionais, etc. Surgiu inclusivamente o TDD (Test Driven Development) que leva a coisa mais longe – os testes devem ser desenvolvidos antes do código em si.

Desde essa primeira experiência com o JUnit que tenho acompanhado a evolução das metodologias  e ferramentas de testes. E tenho tentado utilizá-las nos projectos em que participo. Sem desprimor para os tutoriais e livros sobre testes unitários, os projectos em que participo são um pouco mais complexos que contas bancárias nas quais queremos verificar se o saldo nunca fica negativo. Tive alguma dificuldade em aplicar os princípios simplistas dos livros à complexidade de projectos que dependem de imensas interfaces, desde as várias Bases de Dados envolvidas aos Webservices, passando pela camada gráfica (GUI) e pelo processamento de quantidades massivas de ficheiros. Eu sei que devo substituir as interfaces por Mocks ou Stubs (objectos fictícios que emulam os objectos reais) ou usar Bases de Dados descartáveis que residem em memória só durante a execução dos testes. E fiz isso em vários projectos. Só que deparei-me com dois problemas:

  • O custo de desenvolver testes unitários recorrendo a Mocks é enorme. Testar se o saldo de uma conta bancária nunca fica negativo é simples. Testar se foram feitas as queries correctas nas várias BDs, com múltiplos JOINs, GROUP BY’s e o diabo a quatro, é um pesadelo. Ou temos uma BD completamente populada com todos os casos possíveis e imaginários de acontecer em produção, ou dizemos ao Mock object quais são as queries que devem ser executadas e verificamos se realmente foram executadas (duh!). Mas este não é o problema mais grave…
  • A maioria dos bugs que aparecem em produção não podiam ter sido previstos num teste unitário. Depois de o bug acontecer é (relativamente) simples fazer um teste unitário que o provoque (esta é aliás uma boa prática, nunca corrigir um bug sem primeiro desenvolver o respectivo teste unitário). Mas essa não é a questão. Em software complexo, a menos que os programadores sejam completamente desleixados, os bugs que acontecem em produção resultam de uma combinação de factores que é muito difícil prever. Muitas vezes resultam de coincidências ou de circunstâncias ambientais que nunca acontecerão na máquina de desenvolvimento. Resultam de problemas nas interfaces com outros sistemas e não no algoritmo da aplicação. Este é o problema mais grave.

Por curiosidade, fui à procura de issues abertos no confluence, um wiki desenvolvido pela atlassian que por acaso é também uma das minhas aplicações preferidas e deparei-me com este, relacionado com o RTE (Rich Text Editor) para editar artigos:

Insert line 1 of text into RTE
Insert line 2 of text into rte, highlight text and use ctrl-6 to make it a heading
Use backspace key to back from beginning of line 2 to the end of line 1
Look at the wiki markup. Line 2 will have tags around it for color and an asterisk for bold.

Reparem que este erro nem sequer tem interacções com Bases de Dados ou outras mas resulta de um conjunto de condições quase imprevisíveis (e só acontece em Safari!). Alguém poderia prever este erro? O facto de se desenvolver um teste unitário para resolver este erro dá-nos alguma garantia que não vai aparecer outra combinação estranha de condições no RTE?

A verdade é a seguinte: com mais ou menos custo, todas as funções de uma aplicação podem estar cobertas por testes unitários. Mas o que eu me tenho apercebido é que esses testes, na prática, não reduzem substancialmente o número de bugs em produção. E o que interessa é não ter bugs. Se preferirem o lado económico da questão digo-vos que o custo de cobrir a totalidade de uma aplicação com testes unitários é proibitivo. Cobrir 80% da aplicação é fácil. Mas os bugs com verdadeiro impacto estão normalmente nos 20% que faltam. É verdade que 80% é melhor que nada. Mas eu não me iludo com estes números. Prefiro uma equipa de testes a “bombardear” a aplicação durante uma semana que uma equipa de desenvolvimento a programar testes unitários durante um mês.

O cliente não quer saber se os programadores fazem testes unitários, quer saber se a aplicação não tem erros. No entanto, leio e oiço muitos programadores a gabarem-se que fazem testes unitários e como tal o seu software tem uma qualidade superior. Isso é puro narcisismo baseado em teorias. E pode também ser um placebo muito perigoso, pois corre o risco de tornar os programadores desleixados ou pior ainda, dar ideias a uns quantos gestores de que afinal podemos usar programadores incompetentes pois os testes unitários vão apanhar as borradas deles.

Há quem chame a isto o paradoxo do pesticida, aqui bem descrito:

The Damning Evidence: Things like Test Driven Development and Unit Testing give us the false impression that we’ve quashed the major bugs in the system when all we’ve really done is quash the obvious bugs, leaving the more subtle, painful, and difficult ones behind.  Many of these types of bugs are related to concurrency or particular complex data conditions that are difficult to express as unit tests.

Before anyone rants about this comment section claiming I think TDD is bad, or unit testing is evil, please hear me correctly:  Unit testing and TDD leave a false sense of security that we’ve managed to create stable software. They are a starting point to more complete testing, but they are not the end.  The meaningful problems are often in integration with other systems and modules, that are often left out of testing plans because of time constraints, schedule pressures, laziness and sometimes plain arrogance.

Exceptions: Small, simpler systems rarely suffer from these issues because testing is much easier.  This is mostly a complex software problem, at a level of enterprise development, large applications (e.g. Microsoft Word), or operating systems.

Mais recentemente, houve um pequeno arrufo entre o Bob Martin e os criadores do Stackoverflow precisamente sobre a utilidade dos testes unitários. Eis um excerto do Joel:

There’s a debate over Test Driven Development… should you have unit tests for everything, that kind of stuff… a lot of people write to me, after reading The Joel Test, to say, “You should have a 13th thing on here: Unit Testing, 100% unit tests of all your code.”

And that strikes me as being just a little bit too doctrinaire about something that you may not need. Like, the whole idea of agile programming is not to do things before you need them, but to page-fault them in as needed. I feel like automated testing of everything, a lot of times, is just not going to help you.

Apesar disto tudo, não acho os testes unitários inúteis. Os princípios que estão por trás fazem todo o sentido e vou continuar a acompanhar a evolução das ferramentas nesta área. Sempre que desenvolvo componentes (bibliotecas) que vão ser utilizados e reutilizados em diversos projectos, crio os respectivos testes unitários. Esses componentes são normalmente independentes de factores externos e o custo de desenvolver os respectivos testes é facilmente amortizado pelos vários projectos que os utilizam. Mas a maioria dos projectos não são bibliotecas comuns para reutilização. São aplicações muito específicas e começa a ser difícil justificar o custo, ainda para mais quando não existe uma redução significativa dos bugs que aparecem em produção. Para reduzir bugs, é tremendamente mais efectivo fazer code-review (pair programming é um nome pomposo para code-review, que já existia muito antes do XP) ou ter equipas dedicadas só aos testes.

Resta-me referir uma grande vantagem dos testes unitários de que raramente se fala – documentação: não existe melhor forma de documentar uma função do que escrever os respectivos testes unitários. Bate de longe qualquer documento de especificação e inclusivamente os comentários do código, por uma razão muito simples. Nunca fica obsoleta, caso contrário o teste falha.