Test Driven Development (Desenvolvimento Orientado a Testes) é uma metodologia de desenvolvimento de software bastante popular, que é centrada (adivinhe) em testes.
No entanto, apesar de muito propagada, há várias críticas a essa metodologia por profissionais e pensadores de destaque.
Por esses dias rolou uma conversa bastante interessante sobre o assunto entre, nada mais nada menos, que Martin Fowler (que elaborou a ideia de Injeção de Dependência), Kent Beck (idealizador do TDD e do framework JUnit) e David Heinemeier Hansson (criador do Ruby on Rails).
Vamos analisar os pontos altos da conversa nos próximos tópicos. Mas antes, uma breve revisão sobre TDD…
Como o TDD funciona
Ao contrário do processo “tradicional” de desenvolvimento que envolve Especificação, Implementação e Testes, o TDD começa pelos testes em cima das interfaces do sistema.
A princípio todos os testes irão falhar. O objetivo do desenvolvedor é fazer com que eles sejam executados com sucesso. Em tese, isso garante o foco no que realmente deve ser feito e fornece uma medida mais real do progresso.
Os testes utilizados no TDD geralmente são Testes Unitários. Ambos os conceitos estão muito relacionados, mas não são sinônimos.
Testes Unitários
Testes Unitários são aqueles onde uma única função ou método do sistema é testada de cada vez. Em geral esses testes são automatizados.
Isso pode parecer simples, mas implica em que a rotina em questão não pode depender de outras rotinas, caso contrário o teste é sujeito a falhas de terceiros.
Dessa maneira, a fim de testar as rotinas que naturalmente possuem dependências de outras classes e métodos, de recursos como bancos de dados ou arquivos e outros, os desenvolvedores criam simulações desses elementos para que a rotina a ser testada funcione, porém não dependa diretamente desses outros elementos durante o teste.
Este é o conceito de mock, isto é, algo que imita o elemento original.
Por exemplo, um método que normalmente retorna um valor do banco de dados poderia ser substituído por um método que retorna um valor fixo num teste em particular. Cada linguagem, framework e ferramenta deve prover formas adequadas para se fazer isso, sendo inclusive uma forma de medir a qualidade da mesma.
Benefícios desta abordagem
Escrever os testes antecipadamente e então desenvolver com base neles traz várias vantagens:
- Melhora o entendimento dos requisitos, já que para escrever o teste é necessário que o desenvolver entenda logo de início o que o sistema deve fazer.
- Provê um objetivo claro a ser alcançado durante o desenvolvimento, isto é, fazer os testes executarem sem erros.
- Aumenta a visibilidade do que está ou não pronto, já que os testes servem como indicadores mais confiáveis do que algo que está implementado e “só falta testar”.
- A automação dos testes possibilita a Integração Contínua com certa garantia do funcionamento do sistema, já que todos os testes unitários podem ser reexecutados como testes de regressão sempre que necessário.
Desvantagens
Como regra geral, tudo que traz vantagens tem seus pontos negativos. Sempre temos que tomar cuidado com certos “evangelistas” que irão fazer uma tecnologia ou metodologia parecer não ter um lado negativo ou impacto zero.
Isso se aplica também ao TDD. Vejamos algumas considerações sobre ele:
Aumento do esforço
Será necessário gastar muito tempo para criar os testes e todos os mocks, além da infraestrutura de Integração Contínua. É claro que isso pode ser visto como um investimento em qualidade ao invés de uma pena a ser paga, mas na prática nem todos podem se dar a esse luxo.
Aumento da complexidade
A arquitetura acaba sempre mais complexa, usando muita Injeção de Dependências e outras técnicas de desacoplamento.
Mudanças nos requisitos são muito mais custosas, já que além do código do sistema em si, todos os testes e mocks envolvidos deverão ser reescritos.
Se o arquiteto de software não souber o que está fazendo, a arquitetura do sistema pode acabar cheia de gambiarras.
Intrusividade no código principal
Em muitas situações, será necessário programar de uma forma específica para que um certo código possa ser testado.
A arquitetura da aplicação muitas vezes acaba sendo então afetada por elementos e fatores que, de outra forma, não fariam parte de sua natureza.
Enfim, o design da aplicação acaba sendo influenciado pelos testes, o que nem sempre é desejável, já que é mais uma coisa a ser levada em conta e pode desviar o foco do arquiteto.
Testar tudo é quase impossível
Muitas tecnologias são difíceis de testar com testes unitários ou mesmo com testes de integração.
Pense na interface do usuário, por exemplo. Quantas delas possibilitam a execução da interface de forma “desacoplada”? Existem ferramentas como o Selenium para sistemas web, mas com elas também vêm diversas outras limitações e dependências.
O TDD está morto?
Vimos que todo benefício traz um custo associado. É este todo o ponto da discussão sobre o TDD estar morto. O custo de testar tudo e criar os mocks necessários para todos os testes vale a pena? Vejamos alguns comentários a seguir.
David Hansson inicia a argumentação dizendo que muitas pessoas não conseguem trabalhar com TDD porque não faz muito sentido para determinados tipos de trabalho. Ele toca no ponto da necessidade de se ter muitos mocks e que isso acaba tornando a codificação difícil.
Kent Beck aproveita o gancho e relata de casos onde a equipe não conseguia refatorar o código porque os testes estavam tão acoplados com a implementação, com dois ou três níveis de mocks, que qualquer alteração afetava inúmeros deles. Ele também afirma que há cenários onde o TDD se aplica melhor, por exemplo, quando os requisitos do software a ser desenvolvido são claros o suficiente para escrevê-los em testes diretamente. Por outro lado, há situações onde simplesmente o desenvolvedor vai descobrindo aos poucos aquilo que deve fazer. Isso é recorrente na criação de frameworks ou algoritmos que precisam analisar dados não estruturados.
Fowler argumenta ainda que a maior vantagem do TDD é um código que consegue testar a si mesmo. Só isso poderia também ser alcançado de outras formas. Ele também comenta sobre aqueles que dizem que “você não está fazendo teste unitário de verdade” porque existem algumas dependências durante o teste. Segundo ele, o importante não é a pureza de uma definição, mas que o teste funcione e seja útil, até porque há variações da definição de testes unitários.
Conclusões
É importante entender que TDD não é a “bala de prata” do desenvolvimento de software. Ele traz consigo vários desafios e problemas em potencial, principalmente se não há pessoas suficientemente experientes na equipe.
Testar é essencial, mas para cada projeto podem haver diferentes formas de testes, cujo busto-benefício deve ser avaliado para cada caso.
O teste não deve ser um objetivo em si mesmo, ainda que adotemos o TDD. Ele apenas tem o objetivo de garantir que estamos entregando o que o cliente pediu.
O tempo e o esforço que são investidos em testes também devem ser avaliado. Isso afeta diretamente a qualidade, mas a qualidade tem o seu preço. Quem irá pagar o preço da qualidade?
Quanto a testes unitários, não devemos buscar toda a pureza possível. O importante é que ele teste, ou seja, funcione para aquela situação específica.
Concluindo, o conselho em geral é: use TDD com moderação.
Para quem consegue entender Inglês, aqui vai o link do Hangout com essas grandes personalizados:
Is TDD dead?