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. 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() > 3000.0) {
return funcionario.getSalarioBase() * 0.8;
}
else {
return funcionario.getSalarioBase() * 0.9;
}
}
private double quinzeOuVinteECincoPorCentoDeDescontoNo(Funcionario funcionario) {
if(funcionario.getSalarioBase() > 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() > 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é!