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.

7 Comments

  1. Arzebiu says:

    A minha experiência também vai de acordo ao que dizes. Há uma outra vantagem em termos 80% do código coberto, é que se alguém chegar e mudar algo no código que funciona no que ele tá a testar mas detona um componente extra que ele não testou, os testes supostamente ajudam.

    Outra coisa fundamental, como disseste, é ter um caso de teste por cada bug reportado, embora por vezes isso seja difícil. Também não tenho muito boa experiência com equipas de teste humanas. Presumo que o mesmo é ter um mix dos dois mundos.

    Outra coisa super chata nos testes é quando precisas de fazer refactoring à arquitectura e depois… ups, ainda tenho de actualizar não sei quantos testes.

  2. Rui Salgado says:

    Parece-me haver alguma confusão em relação ao real objectivo dos testes programáticos (sejam unit tests, integration tests, ou end-to-end tests). Os testes programáticos contribuem, de várias formas, para a qualidade interna do software, mas não são prova de infalibilidade do mesmo, e não substituem testes humanos.

    As práticas ditas ágeis defendem inclusivamente a inclusão de testers dedicados a testes exploratórios.

    Prefiro destacar outros benefícios nos testes programáticos, como por exemplo:
    - permitem confiança na introdução de alterações, e a viabilização da prática continua de refactoring;
    - auxiliam na descoberta de um design mais adequado para a solução (se algo se revela difícil de testar, se calhar não está muito bem pensado);
    - aceleram o ciclo de desenvolvimento, oferecendo feedback rápido e detecção de bugs introduzidos;

    Enfim, acho que o que torna os testes programáticos interessantes é olharmos para eles como parte integrante do processo de desenvolvimento de software, com repercussões na qualidade do design interno, documentação, desempenho, extensibilidade, manutenção, etc.

    Como bónus, também ajuda de certa forma a validar que o software faz realmente aquilo que se propõe a fazer :)

  3. João bernardino says:

    Concordo plenamente com a opinião do Rui Salgado.

    Complemento ainda com a minha discordância com a noção que o custo de efectuar testes (especialmente numa perspectiva de Test-Driven Development) não seja justificada.

    Ao desenvolver incrementalmente uma funcionalidade haverão muitos cenários a considerar. Duvido que o custo de a cada iteração de desenvolvimento teres que manualmente confirmar todos os cenários não seja maior que o custo de ir desenvolvendo uma bateria de testes que é corrido de forma automatizada a cada iteração, especialmente se numa qualquer iteração for introduzido um bug que cause erros em cenários anteriores.

    Estas considerações serão especialmente verdade em cenários de alterações necessárias em módulos antigos ou desconhecidos pelo developer responsável.

    Além disso, gostaria de comentar um frase específica:

    “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.”

    Para começar, não percebo como uma boa bateria de testes (unitários, de integração e end-to-end) não ajuda a encontrar bugs que depois são encontrados “a olho”…

    Depois considero as noções de code-review e pair programming distintas:

    Uma code-review formal, é feita à posteriori do desenvolvimento, e é composta por uma análise do código linha a linha.

    Pair programming envolve dois programadores a trabalhar lado a lado, um envolvido directamente com o problema imediato e outro com preocuções mais de alto nível como a direcção geral do desenvolvimento, possíveis melhorias ou problemas a resolver.

    O objectivo do pair programming é a produção de melhores programas, com melhor design, menos bugs e soluções mais elegantes e fáceis de manter.

    Tem ainda um agradável side-effect que é a partilha de conhecimento, aprendizagem de novas técnicas e o colective-code ownership.

    No que diz respeito aos testes vs code-reviews, a completude de uma code review é directamente proporcional ao overhead introduzido pela mesma (mais thoroughness implica maiores custos). É indicada para encontrar problemas no código (memory-leaks, buffer overflows, race conditions, etc) mas não ajuda a encontrar problemas nas regras de negócio, coisa que é possível com os testes.

  4. Alves says:

    Rui, antes de mais obrigado pela tua opinião. Dizes que existe uma confusão sobre o objectivo dos testes programáticos – na tua opinião eles visam aumentar a qualidade do software.

    O problema é a definição de “qualidade de software”. Para os programadores, qualidade de software poderá significar testes unitários, utilização de patterns, código comentado, etc. Mas para os utilizadores, qualidade significa que o software o ajuda a resolver um problema de forma simples, eficiente e sem erros.

    A minha reflexão prende-se precisamente com o (não) impacto dos testes programáticos na qualidade do software tal como é percepcionada pelo utilizador final.

  5. Alves says:

    João, relativamente à minha “provocação” dos code-reviews, deixa-me complementar um pouco:
    - Em “Pair programming”, um dos elementos desenvolve e outro observa. O que observa está, no fundo, a fazer um code-review informal, no sentido em que está a analisar o código tentando detectar falhas.
    - Não sei bem o que queres dizer com code-review formal. Para mim, code-review é simplesmente a revisão do código ou parte deste por pelo menos um elemento da equipa antes de ir para produção.
    - Normalmente quem revê o código é um elemento mais experiente da equipa, e por isso mesmo code-review é a *melhor* forma de encontrar problemas nas regras de negócio, pois os elementos mais novos poderão ainda não conhecer o negócio o suficiente para detectar certas situações.

  6. Rui Salgado says:

    Mencionei especificamente a qualidade *interna* do software.

    Claro que, tipicamente, ao utilizador final pouco importa como o software está feito, desde que resolva o problema em questão e, como bónus, até seja agradável de utilizar.

    Mesmo assim, os testes unitários podem afectar a experiência do utilizador final, tudo depende do tipo de utilizador e do tipo de software! Se for algo desenvolvido para programadores, como uma framework, é bom que a qualidade interna seja boa, e por conseguinte que existam testes compreensivos. Além disso, a má qualidade interna do software esconde muitos custos, que mais tarde ou mais cedo terão que ser imputados a alguém (ou a quem paga pelo software, ou a quem o desenvolve).

    By the way,

    http://jamesshore.com/Agile-Book/pair_programming.html

  7. Francisco Passos says:

    Excelente artigo Alves, já estou a comentar muito após a escrita mas acho que ficou uma coisa por dizer.

    Os testes unitários reduzem a incidência de erros se calhar fáceis de detectar por code review ou pair programming. No entanto é relevante referir que apanhar os bugs mais cedo é melhor do que mais tarde por dois motivos. O primeiro é que o programador está “com a mão na massa” e percebe o enquadramento das suas alterações, portanto corrigir agora é mais simples do que daqui a uns meses. O segundo é que a partir de um conjunto de bugs não detectados começas a ter interacções entre eles que tornam o debug mais complexo e portanto mais caro.

    Se resolvermos apanhar estes com testes, ainda que com cobertura não total (que pode ter um custo demasiado grande e ser ilusório em cima disso), podemos libertar os esforços do code review para problemas de maior complexidade.

1 Trackback

  1. [...] This post was mentioned on Twitter by inospito, Carlos Rodrigues. Carlos Rodrigues said: Para "engenharia", o que não falta são placebos. RT: @inospito: Blog post: Serão os testes unitários meros placebos? http://goo.gl/fb/7PfsF [...]

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*