Por que usar Test Driven Development?

Sempre que escrevo ou introduzo algum assunto, gosto de começar falando das motivações para estudar tal tópico. É muito comum em nós desenvolvedores, achar que novas idéias são viagens, ou então que são muito bonitas na teoria, porém difíceis de aplicar na prática, e coisas do gênero. Por isso, antes de falar qualquer coisa sobre TDD, falarei das motivações – que diga-se de passagem não são poucas – para estudar tal paradigma.
O objetivo deste post é listar, com uma breve explicação, as principais vantagens, ou motivadores, em usar o TDD no desenvolvimento de Software. O grau de importância de cada uma das vantagens é um tanto subjetivo e complicado de ser mensurado. Sendo assim, o leitor não deve ser preocupar com a ordem em que as mesmas serão apresentadas.
Em última instância, estamos sempre interessados em entregar código de qualidade. Quando falamos em qualidade, a primeira coisa que nos vem à cabeça é a quantidade de bugs. De fato, como veremos mais adiante, o TDD nos auxilia a entregar Software com menos defeitos, porém existem outras características importantes em relação à qualidade de código, tais como: acoplamento, coesão, escalabidade, entre outras. Neste post veremos como o TDD pode nos auxiliar a criar código com baixo acoplamento, alta coesão, logo escalável.
Em linhas gerais e de forma muito resumida, nossa missão é entregar código robusto e com alta qualidade. Então, sem mais delongas, vamos ver a seguir o porquê do TDD ser bastante útil nesta missão.

Reduz o GAP entre a introdução, identificação e correção de bugs.

O National Institute of Standards and Technology (NIST), realizou um estudo em 2002 chamado “The Economic Impacts of Inadequate Infrastructure for Software Testing” que constatou que defeitos em Software custaram aos EUA, nada mais nada menos que, 60 BILHÕES de Dólares. O estudo foi realizado em empresas de tamanho e domínio variados. Uma das mais importantes conclusões do estudo foi que este elevado custo ocorre não apenas pela enorme quantidade de defeitos, mas também pelo elevado tempo que um defeito leva para ser identificado e corrigido. Como assim? Felizmente, o mesmo estudo concluiu que o custo de correção de um defeito cresce de forma exponencial ao intervalo de tempo entre a introdução e correção do mesmo. Ora, se o problema é este GAP, só temos uma solução: reduzí-lo o máximo possível. E se pudéssemos reduzí-lo de tal forma que os bugs fossem identificados no ato da codificação? É aí que entra o TDD, pois ele permite que a maioria dos bugs sejam identificados imediatamente após terem sido introduzidos. Esta detecção “real-time” vai lhe poupar de um generoso tempo de depuração e compreensão do código defeituoso. É claro que, mesmo usando TDD, você não tem garantias de que todos os bugs serão identificados. É imporante ressaltar também que as vantagens do TDD podem ser potencializadas com o uso de outras práticas do eXtreme Programming, tais como programação em pares, integração contínua, entre outras.

Segue abaixo o gráfico proposto por Scott Ambler que ilustra bem isto:

A tabela abaixo foi retirada do estudo acima citado e faz um cruzamento entre a fase onde o bug foi introduzido com a fase onde foi identificado e o custo médio em horas de correção deste bug:

Para ilustrar, uma das leituras que podemos fazer a partir da tabela acima é que o custo (em tempo) de correção de um bug que foi introduzido na fase de codificação será três vezes maior caso o mesmo seja identificado após esta fase, podendo chegar a ser cinco vezes maior caso o mesmo seja identificado apenas após o lançamento do produto.

Abaixo segue algumas outras conclusões interessantes publicadas neste trabalho do NIST:

  • 80% do tempo de um desenvolvedor é dedicado à identificação e correção de bugs.
  • Mais de 50% dos bugs não são detectados até a fase de homologação.
  • Aproximadamente 50% dos bugs são introduzidos na fase implementação.
  • Cerca de 50% do orçamento total de um projeto de software é gasto em correção de bugs.

Facilita a criação de código com alta coesão e baixo acoplamento.

Imagine que lhe passem a tarefa de fazer um desenho composto de polígonos regulares. O seu desenho será avaliado em relação a sua semântica e o quão preciso estão os lados e ângulos dos polígonos. Não vamos entrar em detalhes do que seria a semântica (ou harmonia), mas considere que a criação de uma boa semântica seja tão complexa que vai consumir quase todo o tempo que você tem para construir o desenho. Além disso, você não pode utilizar uma régua. Qual o problema desta tarefa? Por melhor desenhista que você possa ser, você vai precisar gastar um bom tempo desenhando e validando se cada figura é um polígono regular. Com isso, você vai ter menos tempo para pensar na parte mais complexa do tarefa. Se você pudesse usar uma régua, você poderia gastar quase todo o tempo da tarefa pensando na “semântica” do desenho, uma vez que a régua lhe auxiliaria a desenhar os polígonos de forma muito mais rápida e precisa. É exatamente assim que ocorre na codificação. Sempre que começamos a codificar um novo pedaço de um sistema (tarefa de desenhar), temos o desejo de criar o melhor código (ou arquitetura) possível (assim como o desenhista deseja construir os polígonos regulares com “boa semântica”). Entretanto, para criar um bom código (ou arquitetura), você precisa a cada novo passo, validar a evolução do mesmo. Essa validação só pode ser feita forçando a execução do seu código. Sem TDD, a única maneira de fazer isso é testar funcionalmente seu código. O problema disto é que, por ser um processo manual, é lento e improdutivo. Além disso, você precisa a cada novo passo na codificação, re-testar tudo o que já fora testado e isso recai sobre o problema de ser um processo manual. E um último problema é que não é tarefa do desenvolvedor gastar muito tempo na execução de testes funcionais (e tão pouco gostamos de fazer isso). Mas como somos espertos, o que fazemos? Simples, codificamos toda a arquitetura num passo só e validamos tudo num único teste funcional. Estratégia pouco inteligente. É como se o desenhista tentasse construir todo o seu desenho e validar a “semântica” apenas após desenhar o último polígono. A chance da “semântica” não ficar boa é grande. Além disso, ao detectar os polígonos culpados pela “semântica” ruim, a correção isolada dos mesmos provavelmente vai causar um efeito colateral na harmonia geral do desenho.

Reduz o tempo gasto em depuração e em correção de bugs.

Quando iniciamos o processo de correção de um bug, a primeira coisa que geralmente fazemos é tentar reproduzir o erro, para em seguida depurar o fluxo onde ocorre o bug para enfim analisar o estado dos objetos em busca da raíz do problema. Quando você não possui testes, você precisa mais uma vez realizar os passos de um teste funcional para depurar. Se você escrever um teste unitário que “estimule” o sistema a passar pelo código defeituoso, você estará “reproduzindo” o erro sem necessidade de executar a aplicação que, como vimos, é bem mais produtivo do que testar funcionalmente o código.

Serve de suporte para testes de regressão.

É comum, ao corrigir um bug, introduzir um novo bug. Em outras palavras, a correção de um bug pode ter como efeito colateral que uma parte do software que antes funcionava deixe de funcionar. Quando isto ocorre, dizemos que o software regrediu. Chamam-se “teste de regressão” os testes que visam verificar a integridade geral do sistema quando um bug é corrigido, ou até mesmo quando uma nova funcionalidade é implementada no sistema. O uso do TDD vai reduzir a introdução de efeito colaterais junto com alterações no código. É claro que apenas na parte do código que seja coberta pelo testes unitários. Além disso, quando um teste passa a falhar após sua alteração, você acabou de identificar o bug num momento muito próximo de sua introdução, o que é uma enorme vantagem como vimos acima.

Encoraja o refactoring.

O refactoring é uma atividade bastante importante dentro do processo de desenvolvimento de software. É através dele que mantemos o código legível e escalável. Quanto mais tempo ficamos sem refatorar o código, mas ele vai “perecendo”. Em outras palavras, quanto mais tempo ficamos sem refatorar, mais custoso e arriscado será mudá-lo. Entretanto, inúmeras vezes em nossa carreira temos medo de alterar um código. Esse medo muitas vezes ainda é ratificado por um colega que diz: “Não mexe aí que está funcionando!”. Esse medo vem do fato de termos ciência de que ao alterar um código que funciona, a chance de não testar todos os casos onde código é estimulado é muito grande. Além disso, algumas mudanças requerem testes exaustivos que muitas vezes não temos tempo hábil para realizar. Quando usa-se TDD, a execução dos testes é automatizada e temos garantia que todos os casos onde temos cobertura serão testados. Realizando TDD e refactoring com frequência acaba tendo como resultado um ciclo vicioso positivo na qualidade do código, pois uma vez que o processo lhe permite refatorar de forma mais constante, o código apresenta uma melhoria continua o quê o torna cada vez mais testável, ou seja, “realimentando” a coragem em refatorá-lo.

Serve como documentação.

Testes unitários, se bem escritos, podem servir como forma de documentação do sistema, onde o comportamento do sistema é descrito pelos testes. Além disso, os testes também podem servir como forma de documentação de uso de alguma API, ou framework do qual o sistema dependa.

Referências.

1) “The Economic Impacts of Inadequate Infrastructure for Software Testing”, National Institute of Standards & Technology (NIST), US, 2002
2) “Introduction to Test Driven Design (TDD)”, http://www.agiledata.org/essays/tdd.html, acessado em 2010

  1. Anna says:

    Acho que o grande problema com o uso de testes é que a maioria das empresas não tem essa cultura o que “força” o programador a fazer testes funcionais. Outra coisa é que o ensino na maioria das faculdades e cursos em geral não mostram testes, quando mostram o fazem justamente depois que todo o programa foi codificado o que induz ao hábito de fazer testes apenas depois. Enfim, concordo com você que testes são importantes e mais ainda que desenvolver orientado a testes, apesar de díficil (eu ainda estou tentando aprender e me acostumar) é necessário.

  2. Pois é, eu também estou tentando aprender e me acostumar também, faz pouco tempo que comecei a estudar programação e menos tempo ainda que fui apresentado ao TDD, e mesmo lendo um ótimo artigo bem grande e explicativo como esse eu ainda não consigo enxergar bem o uso do TDD no meu dia-a-dia na programação, talvez porque eu ainda não tenha entendido como montar um TDD direito ou porque como a Anna comentou nós não somos apresentados a isso na faculdade ou no começo dos estudos, o que faz com que a gente aprenda errado e depois fica difícil para mudar e acostumar…

    Mas mesmo assim eu estou disposto à aderir ao conceito de usar o TDD no dia-a-dia, acho que pode dar certo…

    Ótimo artigo, obrigado e fique com Deus!

  1. There are no trackbacks for this post yet.

Leave a Reply