Agile Brazil – TDD
Autor: Pedro Gonçalves | Publicado: 2011-07-13T05:51:00-07:00

Um assunto muito abordado durante o Agile Brazil 2011 foi o Test-driven development  (TDD). É um assunto que não domino e sempre tive curiosidade de  estudar. Assisti algumas palestras/workshops e a idéia é realmente muito  bacana, não é atoa que tem tanta gente utilizando por ai. Vou tentar  passar uma visão geral, do pouco que aprendi, sobre TDD neste post.
A ideia é simples:
Escreva o teste, ele vai falhar;
Escreva o código, faça o teste passar;
Refatore o código, pense no que escreveu, tente melhorar. 

Pronto! Basta refazer esse ciclo a cada nova funcionalidade que  deseja criar, e garanta que seu teste esteja. Mas para que fizemos isso?  Será que é apenas para termos um percentual de teste maior? Será que  realmente foi para garantir que o nosso sistema está sendo testado? Não! TDD não diz respeito apenas a teste, TDD é também uma técnica design!

Segundo Bob Martin, “The  act of writing a unit test is more an act of design than of  verification.&nbsp; It is also more an act of documentation than of  verification.” TDD pode encaminhar a um nível que possibilite um  código mais modularizado, flexível e extensível. Este efeito surge  devido a  metodologia requerer que os desenvolvedores pensem no software  em  pequenas unidades que podem ser re-escritas, desenvolvidas e  testadas  independemente e integradas depois. Isto implica menores e  mais classes,  evitando o acoplamento e permitindo interfaces mais  limpas (Wikipedia). Resumido por Martin Flower, “The  benefit is that  thinking about the test first forces you to think  about the  interface to the code first. This focus on interface and how  you use  a class helps you separate interface from implementation.”

Em um dos workshops que participei, o pessoal do Caelum  apresentou de maneira prática alguns bons exemplos de como o TDD pode  nos ajudar a enxergar alguns problemas de design nas nossas aplicações.  Por exemplo, nos deram o seguinte código, o qual é responsável por  calcular salários dos funcionários de acordo com seu cargo e salário  base:
import static br.com.caelum.tdd.exercicio1.Cargo.DBA;
import static br.com.caelum.tdd.exercicio1.Cargo.DESENVOLVEDOR;
import static br.com.caelum.tdd.exercicio1.Cargo.TESTADOR;

public class CalculadoraDeSalario {

   public double calcula(Funcionario funcionario) {
       if(DESENVOLVEDOR.equals(funcionario.getCargo())) {
          return dezOuVintePorCentoDeDescontoNo(funcionario);
       }
       if(DBA.equals(funcionario.getCargo()) || TESTADOR.equals(funcionario.getCargo())) {
          return quinzeOuVinteECincoPorCentoDeDescontoNo(funcionario);
       }
       throw new RuntimeException("funcionario invalido");
   }
   
   private double dezOuVintePorCentoDeDescontoNo(Funcionario funcionario) {
       if(funcionario.getSalarioBase() &gt; 3000.0) {
          return funcionario.getSalarioBase() * 0.8;
       }
       else {
          return funcionario.getSalarioBase() * 0.9;
       }
   }

   private double quinzeOuVinteECincoPorCentoDeDescontoNo(Funcionario funcionario) {
       if(funcionario.getSalarioBase() &gt; 2000.0) {
          return funcionario.getSalarioBase() * 0.75;
       }
       else {
          return funcionario.getSalarioBase() * 0.85;
       }
   }
}
public enum Cargo {
   DESENVOLVEDOR,
   DBA,
   TESTADOR
}
Que já possuia os testes unitários implementados:
public class CalculadoraDeSalarioTests {

 private CalculadoraDeSalario calculadora;

 @Before
 public void setUp() {
  calculadora = new CalculadoraDeSalario();
 }
 
 @Test
 public void deveRetornar4000MenosImpostosDe20PorCentoSeDesenvolvedorGanhaMaisDe3000(){
  Funcionario desenvolvedor = umFuncionario(DESENVOLVEDOR, comSalarioBase(4000.0));
  
  double salario = calculadora.calcula(desenvolvedor);
  
  assertEquals(4000.0 * 0.8, salario, 0.000001);
 }

 @Test
 public void deveRetornar1000MenosImpostosDe10PorCentoSeDesenvolvedorGanhaMenosDe3000(){
  Funcionario desenvolvedor = umFuncionario(DESENVOLVEDOR, comSalarioBase(1000.0));
  
  double salario = calculadora.calcula(desenvolvedor);
  
  assertEquals(1000.0 * 0.9, salario, 0.000001);
 }


 @Test
 public void deveRetornar4000MenosImpostosDe25PorCentoSeDBAGanhaMaisDe2000(){
  Funcionario dba = umFuncionario(DBA, comSalarioBase(4000.0));
  
  double salario = calculadora.calcula(dba);
  
  assertEquals(4000.0 * 0.75, salario, 0.000001);
 }

 @Test
 public void deveRetornar1000MenosImpostosDe15PorCentoSeDBAGanhaMenosDe2000(){
  Funcionario dba = umFuncionario(DBA, comSalarioBase(1000.0));
  
  double salario = calculadora.calcula(dba);
  
  assertEquals(1000.0 * 0.85, salario, 0.000001);
 }

 @Test
 public void deveRetornar4000MenosImpostosDe25PorCentoSeTestadorGanhaMaisDe2000(){
  Funcionario testador = umFuncionario(TESTADOR, comSalarioBase(4000.0));
  
  double salario = calculadora.calcula(testador);
  
  assertEquals(4000.0 * 0.75, salario, 0.000001);
 }

 @Test
 public void deveRetornar1000MenosImpostosDe15PorCentoSeTestadorGanhaMenosDe2000(){
  Funcionario testador = umFuncionario(TESTADOR, comSalarioBase(1000.0));
  
  double salario = calculadora.calcula(testador);
  
  assertEquals(1000.0 * 0.85, salario, 0.000001);
 }
 
  
 private Funcionario umFuncionario(Cargo cargo, double salario) {
  Funcionario funcionario = new Funcionario();
  funcionario.setNome("Martin Fowler");
  funcionario.setSalarioBase(salario);
  funcionario.setCargo(cargo);
  return funcionario;
 }
 

 private double comSalarioBase(double salario) {
  return salario;
 }
}E pediram para que fosse inserido um novo cargo, “Gerente”, no qual  deveria incidir imposto de 20% caso o salário base fosse acima de 5000,  ou 15% caso contrário. Como você implementaria esse novo requisito? 100%  dos participantes criaram o novo cargo na enumeração e adicionaram mais  um If na calculadora, solução simples e rápida! E qual o problema disso?
Aumento da complexidade e da complexidade ciclomática;
Proliferação de if´s similares;
Sempre que um novo cargo surgir, alguém deverá lembrar de alterar a calculadora;
O próprio teste aponta os erros!
Observe o nome do teste: deveRetornar4000MenosImpostosDe20SeDesenvolvedorGanhaMaisDe3000 – Existe um condicional no nome do teste!
Existe um teste para cada cargo, mesmo estando todos juntos.
Grande quantidade de teste.
Essa classe não é coesa, ela faz muita coisa!
Não seria melhor implementar assim?
public interface CalculoDeImposto {
    double calculaImposto(Funcionario funcionario);
}
public class DezOuVintePorCento implements CalculoDeImposto {
    public double calculaImposto(Funcionario funcionario) {
       if(funcionario.getSalarioBase() &gt; 3000) {
          return funcionario.getSalarioBase()*0.8;
       }
       else {
          return funcionario.getSalarioBase()*0.9;
       }
    }
}
E no enum:
public enum Cargo {
    DESENVOLVEDOR (new DezOuVintePorCento()),
    DBA (new QuinzeOuVinteECincoPorCento()),
    TESTADOR (new QuinzeOuVinteECincoPorCento()),
    GERENTE (new QuinzeOuVintePorCento());

    private CalculoDeImposto calculadoraImposto;
    Cargo(CalculoDeImposto calculadoraImposto) {
       this.calculadoraImposto = calculadoraImposto;
    }
   
    public CalculoDeImposto getCalculadora() {
       return calculadoraImposto;
    }
}Com isso temos alguns ganhos:
Extrair cada responsabilidade para uma classe específica (Princípio da Responsabilidade Única).
Temos 3 estratégias de cálculo diferentes (10ou20%, 15ou25%, 15ou20%), cada uma deve estar em uma classe separada (Strategy?).
Utilize polimorfismo para que a cada novo cargo não precisemos lembrar de alterar uma classe para dar suporte.
Bom, apenas um pequeno exemplo. É um assunto que vale a pena uma lida e testar para ver os resultados.

Até!