TDD no Mundo Real: eliminando bugs para sempre
Aprenda como a prática do TDD pode eliminar um bug definitivamente do seu software
Pior do que seu cliente encontrar um bug no sistema é ele encontrar um bug que foi dito como corrigido mas que reapareceu após algumas versões. Sempre que um bug reincidente volta, a confiança do cliente no time é minada - com razão. Como podemos ver, não basta corrigir o bug, precisamos de alguma forma corrigi-lo e garantir que ele não volte mais. Mas como podemos fazer isso de forma rápida, segura e a um preço justo?
Antes de responder a esta pergunta, vamos refletir um pouco sobre o que fazemos no dia a dia quando um cliente encontra um bug no sistema. Após o cliente ligar e informar sobre o bug, a tarefa do desenvolvedor é idenficá-lo no sistema. Esse processo pode levar alguns minutos como também pode levar algumas horas dependendo do bug e da experiência do desenvolvedor responsável por corrigí-lo. De qualquer forma, existem diversas estratégias para um desenvolvedor identificar um bug no sistema. Entre elas podemos citar:
- refazer os passos do cliente;
- pedir o screenshot da tela para o cliente;
- testar a aplicação até cair no problema;
- debugar linha a linha da funcionalidade;
- olhar os arquivos de logs para identificar possíveis erros;
Sem dúvida, uma estratégia comum é explorar a funcionalidade com base nos relatos ou na visão do cliente até encontrar o problema. Após confirmar a existência do bug, a próxima tarefa é simular o bug dentro do sistema, ou seja, reproduzí-lo na nossa máquina. Ele precisa ser reproduzível para facilitar sua correção.
Com o bug simulado na nossa máquina local, basta encontrar qual o código problemático e em seguida corrigí-lo, certo? Mas será que isso é suficiente para garantir que o bug nunca mais reapareça?
O que fazer primeiro: corrigir ou escrever o teste?
Agora que encontramos o bug, é só uma questão de corrigí-lo, certo?
Provavelmente esta seria a ação da maioria dos desenvolvedores, pois se trata de um bug e quanto mais rápido o corrigimos mais rápido o cliente ficará satisfeito e poderá usar o sistema. Apesar de fazer sentido, não é bem assim que funciona desenvolvimento de software. Simplesmente identificar o código problemático e corrigir o bug pode trazer mais dores de cabeça do que o desenvolvedor imagina.
Corrigir um bug nas pressas muitas vezes leva a um código cheio de remendos, com baixa legibilidade, design pobre e que dificulta a vida do próximo que for mantê-lo; entre tantos problemas, um dos mais frustrantes para o cliente e para equipe é corrigir o bug e ele reaparecer após algumas versões.
Precisamos de alguma forma corrigir o bug e garantir que ele não volte. De preferência de forma rápida, segura e automatizada. Nesse momento, a melhor solução é escrever um teste automatizado simulando o cenário em que o problema acontece.
Veja que não é simplesmente escrever o teste, mas sim escrevê-lo antes da correção. A verdade é que, escrever um teste para simular o bug antes de corrigí-lo é uma boa prática. Deste modo, caso ele volte os testes nos dirão e poderemos fazer a correção antes de colocar a nova versão do sistema em produção.
Em termos práticos, faremos o seguinte:
- Encontramos o cenário do bug;
- Escrevemos um teste que replica esse cenário e, portanto, falha assim que o escrevemos;
- Aí sim, corrigimos o bug;
- Rodamos novamente o teste, que agora deve passar;
Repare que este processo é de certa forma muito simples, e tem foco na resolução do problema de tal maneira que o bug não volte a acontecer. Não só isso, seguir esta disciplina a cada novo bug que surge trará benefícios imensos a médio-longo prazo.
Usando TDD para corrigir bugs
Para entender como você pode aplicar TDD no seu dia dia, deixa eu te contar uma história real de como fazemos aqui na TriadWorks...
Há alguns anos atrás eu e um estagiário, Mario Diniz, estávamos desenvolvendo o módulo financeiro de um sistema de uma clínica de fisioterapia. Entre as diversas funcionalidades havia uma muito importante que era o parcelamento de pagamentos. Quando um cliente da clínica tinha que pagar uma consulta ele poderia optar por parcelar o pagamento. Para resolver esta funcionalidade, nosso estagiário implementou o seguinte código:
public class Parcelador {
public List<BigDecimal> parcela(BigDecimal valor, BigDecimal numeroDeParcelas) {
BigDecimal valorDaParcela = valor.divide(numeroDeParcelas, 2, RoundingMode.DOWN);
List<BigDecimal> valoresDasParcelas = new ArrayList<>();
for (int i = 0; i < numeroDeParcelas.intValue(); i++) {
valoresDasParcelas.add(valorDaParcela);
}
return valoresDasParcelas;
}
}
O método parcela()
recebe o valor total do pagamento e o número de parcelas na qual o valor deve ser parcelado. No fim, a classe retorna uma lista com as parcelas. Para não perder o costume, junto com o código de produção o Mario também escreveu o seguinte teste de unidade para garantir que o código funcionava como esperado:
public class ParceladorTest {
@Test
public void deveParcelarPagamento() {
// cenario
BigDecimal pagamento = new BigDecimal("999.00");
BigDecimal numeroDeParcelas = new BigDecimal("3");
// ação
Parcelador parcelador = new Parcelador();
List<BigDecimal> parcelas = parcelador.parcela(pagamento, numeroDeParcelas);
// validação
BigDecimal valorDaParcelaEsperado = new BigDecimal("333.00");
assertEquals("total de parcelas", 3, parcelas.size());
assertEquals("parcela #1", valorDaParcelaEsperado, parcelas.get(0));
assertEquals("parcela #2", valorDaParcelaEsperado, parcelas.get(1));
assertEquals("parcela #3", valorDaParcelaEsperado, parcelas.get(2));
}
}
Código compilando e testes verdes, então colocamos a nova funcionalidade no ar. O código funcionou muito bem por um tempo, até que um certo dia um cliente tentou parcelar um pagamento no valor de R$1000 em 3x no cartão de crédito. Aí já viu né, quando o usuário rodou o parcelamento automático ele teve uma bela de uma surpresa!
O sistema gerou 3 parcelas iguais no valor de R$333,33 quando na verdade ele deveria ter gerado a última parcela com valor R$333,34 (1 centavo a mais). Afinal, R$1000 divido por 3 não dá uma número fechado como gostaríamos (R$333,333333...). Tínhamos um bug em mãos e tínhamos que resolvê-lo o mais rápido possível, caso contrário a clinica perderia alguns centavos a cada transação financeira.
Nesse momento o Mario se desesperou, pois ele estava ciente de que a clinica estava perdendo dinheiro e ele se sentia responsável. De imediato ele baixou a última versão da aplicação do Github, identificou o bug, conseguiu simulá-lo no Eclipse e foi codificando para corrigi-lo. Foi aí que eu sentei calmamente ao lado dele, olhei o que ele estava fazendo e pedi para que ele parasse de implementar aquela correção.
Expliquei que deveríamos manter a tranquilidade em momentos como este e não pular etapas; que deveríamos corrigir o bug mas também garantir que ele não voltasse mais; foi então que sugeri que ele invertesse os passos: primeiro ele iria escrever um teste para simular o bug, ver o teste falhar e só em seguida implementar a correção no código de produção.
Basicamente sugeri que ele exercitasse seus conhecimentos de TDD (Test Driven Development). Apesar dele ter estudado sobre testes automatizados ele não havia aplicado a prática ainda, logo se sentia um pouco desconfortável com ela; dei algumas orientações e ele assim desenrolou o restante com maestria. Ele identificou o código problemático e em seguida escreveu o seguinte teste para simular o bug:
@Test
public void deveParcelarPagamentoEColocarADiferencaNaUltimaParcela() {
// cenario
BigDecimal pagamento = new BigDecimal("1000.00");
BigDecimal numeroDeParcelas = new BigDecimal("3");
// ação
Parcelador parcelador = new Parcelador();
List<BigDecimal> parcelas = parcelador.parcela(pagamento, numeroDeParcelas);
// validação
BigDecimal valorDaParcelaEsperado = new BigDecimal("333.33");
BigDecimal valorComDiferencaEsperado = new BigDecimal("333.34");
assertEquals("total de parcelas", 3, parcelas.size());
assertEquals("valor da parcela #1", valorDaParcelaEsperado, parcelas.get(0));
assertEquals("valor da parcela #2", valorDaParcelaEsperado, parcelas.get(1));
assertEquals("valor da parcela #3", valorComDiferencaEsperado, parcelas.get(2));
}
Repare que o cenário do teste foi o mesmo reportado pelo cliente do sistema. Nesse caso, o teste garante que a última parcela deverá ter o valor R$333,34. Pronto! Com o teste escrito, o próximo passo foi rodá-lo e vê-lo falhar:
Teste rodando e falhando como esperado! Isso é ótimo, pois deixa claro que acabamos de simular o bug de forma automatizada. Agora podemos escrever o código suficiente para corrigir o bug! E foi o que o estagiário fez:
public class Parcelador {
public List<BigDecimal> parcela(BigDecimal valor, BigDecimal numeroDeParcelas) {
BigDecimal valorDaParcela = valor.divide(numeroDeParcelas, 2, RoundingMode.DOWN);
BigDecimal resto = valor.subtract(valorDaParcela.multiply(numeroDeParcelas));
List<BigDecimal> valoresDasParcelas = new ArrayList<>();
for (int i = 0; i < numeroDeParcelas.intValue() -1; i++) {
valoresDasParcelas.add(valorDaParcela);
}
// adiciona resto na ultima parcela
valoresDasParcelas.add(valorDaParcela.add(resto));
return valoresDasParcelas;
}
}
Correção feita! Hora de rodar a bateria de testes para ver se o código implementado realmente corrige o bug:
Perfeito! Não só nosso novo teste passou como os testes anteriores continuaram verdes! Isso prova que além de corrigirmos o bug nós não introduzimos nenhum outro bug sorrateiro no sistema. Este é um dos principais valores dos testes automatizados: temos segurança de corrigir, modificar e evoluir o código sem medo de quebrar outras partes do sistema.
Por fim, antes de sairmos pro almoço nós deployamos a aplicação em produção e informarmos ao cliente que o bug havia sido resolvido. Missão cumprida!
Concluindo
Repare na disciplina que usamos para corrigir o bug no sistema: escrevemos um teste que falha; implementamos o código de produção suficiente para ele passar; vimos o teste passar. Estes passos lembram o que?
Sem dúvida acabamos de aplicar TDD na correção do bug, seguimos seu ciclo de desenvolvimento! Usar TDD para identificar, simular e corrigir bugs é uma prática comum entre os desenvolvedores, além de muito recomendada. Não é por acaso que temos um capítulo inteiro dedicado a este tema no nosso curso de TDD e Testes Automatizados em Java.
Lembre-se, quando surge um bug no sistema, não somos obrigados a primeiramente escrever um teste para só depois fazer a correção; aliás, não somos nem obrigados a escrever um teste. Contudo, está claro as inúmeras vantagens que ganhamos ao fazer isso:
- Temos certeza que simulamos o bug;
- Temos certeza que corrigimos ele;
- E o melhor: temos certeza que ele não volta;
Como podemos ver, TDD não só nos ajuda a programar mais focado no problema e investir em um bom design de código, como também nos ajuda a simular bugs dentro do sistema e corrigí-los. Escrever um teste antes da correção do bug é uma disciplina importante que fará com que nossa bateria de testes cresça durante a evolução do sistema. No fim de tudo, a equipe terá confiança no código que produz e, principalmente, segurança na manutenção do código.
E você, como tem feito para eliminar os bugs do seu sistema?
You might also be interested in these articles...
Desenvolvedor e instrutor na TriadWorks