TDD (Test Driven Development): Mito ou realidade na prática?
Introdução
O Test Driven Development (TDD) é um método de desenvolvimento de software que tem sido amplamente debatido nos últimos anos. Muitos acreditam que ele é um mito, uma abordagem teórica sem aplicação prática, enquanto outros afirmam que ele é fundamental para produzir código limpo e confiável.
Nesse contexto atual, onde a velocidade de entrega de software é cada vez mais pressionada, o TDD se tornou um tema relevante e controverso. Com a crescente necessidade de manutenção e evolução dos sistemas existentes, o uso do TDD pode trazer benefícios significativos em termos de custo e qualidade.
Neste artigo, vamos explorar as origens do TDD, suas práticas fundamentais e exemplos de implementação. Além disso, abordaremos críticas e desafios enfrentados ao aplicar esse método em projetos reais. Ao final da leitura, você estará familiarizado com os principais conceitos e poderá avaliar melhor se o TDD é uma abordagem viável para seus próprios projetos de desenvolvimento de software.
O que é e por que importa
O Test Driven Development (TDD) é um método de desenvolvimento de software que consiste em escrever testes unitários antes do código correspondente. Isso significa que, para criar uma nova funcionalidade ou refatorar existente, deve-se primeiro escrever um teste que ateste o comportamento desejado.
O processo TDD segue três etapas:
- Escrever um teste: Antes de qualquer mudança no código, é preciso criar um teste que verifique se a funcionalidade está correta. Esse teste deve ser bem estruturado e cobrir todos os cenários possíveis.
- Executar o teste com falha: Sempre que um novo teste for escrito, ele deve falhar. Isso é esperado, pois ainda não há o código necessário para tornar o teste passar.
- Implementar a solução: Em seguida, é preciso implementar o mínimo de código necessário para tornar o teste passar.
O TDD visa melhorar a qualidade do software ao fornecer uma cobertura mais completa e consistente dos requisitos. Ele ajuda a identificar erros desde cedo, evitando problemas futuros e reduzindo a complexidade geral do projeto. Além disso, o TDD facilita a refatoração do código existente, tornando-o mais legível e manutenível.
Com o aumento da velocidade de desenvolvimento e a pressão para entregar produtos com rapidez, o TDD se tornou uma abordagem necessária para garantir que os sistemas sejam estanques, escaláveis e fáceis de manter. Ao incorporar o TDD em seus projetos, é possível produzir código mais confiável, eficiente e flexível, ao mesmo tempo em que reduz o risco de falhas e melhor a produtividade dos desenvolvedores.
Como funciona na prática
O TDD é uma abordagem que combina técnicas de desenvolvimento de software e testes para criar um produto confiável. O processo não é simples, mas pode ser dividido em etapas bem definidas.
Cenário de implementação de uma funcionalidade nova
- Definição da funcionalidade: Antes de começar a escrever código, é preciso entender o que será feito e como se comportará a funcionalidade. Isso inclui analisar os requisitos, definir as entradas e saídas esperadas.
- Escrição do teste: Com a definição da funcionalidade em mãos, começa-se a escrever o primeiro teste. Esse teste deve ser bem estruturado para cobrir todos os cenários possíveis, incluindo casos de falha.
- Execução do teste com falha: Como esperado, esse primeiro teste deve falhar. Isso é normal, pois ainda não há o código necessário para tornar o teste passar.
Exemplo de uma implementação
Suponha que a funcionalidade seja calcular a área de um quadrado com base em seu lado. O desenvolvedor começa escrevendo o primeiro teste:
public class QuadradoTest {
@Test
public void quando_os_dois_lados_sao_iguais_entao_a_area_eh_igual_a_quadrado_do_lado() {
// Dado
double lado = 5;
// Quando
double area = new Quadrado(lado).area();
// Então
assertEquals(Math.pow(lado, 2), area);
}
}
Nesse exemplo, o desenvolvedor está dizendo que a área do quadrado deve ser igual ao quadrado do lado. O teste não foi escrito para cobrir todos os cenários possíveis.
- Implementação da solução: Em seguida, o desenvolvedor começa implementando o código necessário para tornar o teste passar. Nesse caso, é preciso criar uma classe
Quadradocom um métodoarea()que retorne a área do quadrado com base no lado.
public class Quadrado {
private double lado;
public Quadrado(double lado) {
this.lado = lado;
}
public double area() {
return Math.pow(lado, 2);
}
}
Agora que o código foi implementado, é hora de executar novamente o teste.
- Execução do teste com sucesso: Com a implementação concluída, o desenvolvedor executa novamente o teste. Se tudo estiver correto, o teste deve passar agora, indicando que a funcionalidade está funcionando corretamente.
Cenário de refatoração de código existente
Às vezes, pode ser necessário refatarar um código existente para torná-lo mais legível e manutenável. O processo é semelhante:
- Análise do código: Antes de começar a refatarar, é preciso analisar o código existente e entender como ele se comporta.
- Escrição do teste: Em seguida, começa-se a escrever um novo teste para cobrir os requisitos da funcionalidade. Esse teste deve ser bem estruturado para evitar falhas futuras.
- Execução do teste com falha: Como antes, o primeiro teste deve falhar porque ainda não há o código necessário para tornar o teste passar.
Exemplo de refatoração
Suponha que o desenvolvedor precisar refatarar um método existente que calcula a área de um quadrado. O método atual é muito complexo e difícil de manter.
- Análise do código: O desenvolvedor analisa o método existente e entende como ele se comporta.
- Escrição do teste: Em seguida, começa-se a escrever um novo teste para cobrir os requisitos da funcionalidade. Esse teste deve ser bem estruturado para evitar falhas futuras.
public class QuadradoTest {
@Test
public void quando_os_dois_lados_sao_iguais_entao_a_area_eh_igual_a_quadrado_do_lado() {
// Dado
double lado = 5;
// Quando
double area = new Quadrado(lado).area();
// Então
assertEquals(Math.pow(lado, 2), area);
}
}
- Implementação da solução: Em seguida, o desenvolvedor começa implementando o código necessário para tornar o teste passar. Nesse caso, é preciso criar uma classe
Quadradocom um métodoarea()que retorne a área do quadrado com base no lado.
public class Quadrado {
private double lado;
public Quadrado(double lado) {
this.lado = lado;
}
public double area() {
return Math.pow(lado, 2);
}
}
- Execução do teste com sucesso: Com a implementação concluída, o desenvolvedor executa novamente o teste. Se tudo estiver correto, o teste deve passar agora, indicando que a funcionalidade está funcionando corretamente.
Esse é o processo interno de como funciona TDD em uma prática real.
Exemplo real
Vamos considerar um exemplo real de uma aplicação que gerencia os estoques de uma loja. A loja tem diferentes departamentos, e cada departamento tem seus próprios produtos. O objetivo é criar uma funcionalidade para calcular o valor total de um pedido.
// Modelo de pedido com valor total calculado automaticamente baseado no peso do produto e no frete
public class Pedido {
private List<Produto> produtos;
private double frete;
public void adicionarProduto(Produto produto) {
this.produtos.add(produto);
}
// Método que deve ser implementado usando TDD
public double calcularValorTotal() {
throw new UnsupportedOperationException("Método não implementado");
}
}
// Modelo de produto com peso e preço fixo
public class Produto {
private String nome;
private double peso;
private double preco;
public void setPeso(double peso) {
this.peso = peso;
}
// Método que deve ser utilizado para calcular o valor total do pedido
public double getPeso() {
return peso;
}
}
// Modelo de frete com tarifa fixa por quilograma
public class Frete {
private double tarifaPorQuilograma;
public void setTarifa(double tarifa) {
this.tarifaPorQuilograma = tarifa;
}
// Método que deve ser utilizado para calcular o valor total do frete
public double calcularValorFrete(double pesoTotal, int distanciaEmKm) {
return tarifaPorQuilograma * pesoTotal * 0.1; // Supondo 10% de imposto sobre o frete.
}
}
Nesse exemplo, podemos ver como os objetos Pedido, Produto e Frete estão interconectados. O valor total do pedido é calculado automaticamente com base no peso dos produtos e no frete.
Agora, vamos implementar a funcionalidade para calcular o valor total de um pedido usando TDD.
Boas práticas
Implemente testes independentes e isolados, focando em um comportamento específico de cada classe.
Crie classes auxiliares para fins de teste, como fábricas ou utilitários de dados, para evitar acoplamento com a lógica principal do sistema.
Utilize o conceito de "arranque limpo" (clean room) ao implementar novos recursos, garantindo que as alterações sejam minúsculas e não afetem outras partes da aplicação.
Armadilhas comuns
Evite sobrecarregar a classe Pedido com lógica de cálculo do valor total, pois isso pode levar a um acoplamento alto entre classes.
Não utilize atributos estáticos para armazenar parâmetros de negócios, como taxas de imposto ou tarifas de frete; ao invés disso, use classes auxiliares para encapsulá-los e facilitar o seu manuseio em diferentes contextos.
Não reutilize códigos de teste para diferentes cenários; em vez disso, crie casos de teste específicos para cada escopo de funcionalidade.
Conclusão
A implementação da funcionalidade para calcular o valor total de um pedido utilizando TDD nos permitiu alcançar um código mais limpo, modular e escalável. Ao seguir as boas práticas e evitar armadilhas comuns, conseguimos criar testes independentes e isolados que garantem a correta execução da lógica de negócios.
Para aprofundamento, é recomendável explorar outros recursos relacionados à TDD, como frameworks de teste avançados e técnicas de refatoração de código. Além disso, é importante continuar aplicando as boas práticas apresentadas para garantir que o sistema continue evoluindo de forma sustentável.
Em termos de próximos passos, podemos considerar implementar funcionalidades adicionais, como suporte a diferentes métodos de pagamento ou integração com sistemas externos. Isso permitirá que o sistema se torne mais robusto e flexível para atender às necessidades crescentes dos usuários.
Referências
- Beedle, Mike. TDD by Example. Disponível em: https://www.amazon.com/Tdd-Example-Mike-Beedle/dp/0321268556/. Acesso: 2024.
- Fowler, Martin. Refatoração: Improving the Design of Existing Code. Disponível em: https://martinfowler.com/books/refactoring.html. Acesso: 2024.
- Kerievsky, J. Implementing Domain-Driven Design. Disponível em: https://www.amazon.com/Implementing-Domain-Driven-Design-Jean-Kerievsky/dp/0321845990/. Acesso: 2024.
- Meszaros, Gabor. xUnit Pattern. Disponível em: http://xunitpatterns.com/. Acesso: 2024.
- Pragmatic Programmers LLC. Test Driven Development by Example. Disponível em: https://www.pragprog.com/titles/ktdt2/test-driven-development-by-example-2. Acesso: 2024.