Baixe o app para aproveitar ainda mais
Prévia do material em texto
86964 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Essa talvez seja a seção mais difícil de se escrever, pois a quantidade de pessoas que participaram direta ou indiretamente do livro é muito grande. Vou começar agradecendo a meu pai, mãe e irmão, que a todo momento me apoiaram na decisão de fazer um mestrado, entender como ciência deve ser feita, e que sofreram junto comigo nos momentos de grande stress (que todo mestrado proporciona!). Agradeço também ao meu orientador de mestrado e doutorado, prof. Dr. Marco Aurelio Gerosa, que me ensinou como as coisas funcionam "do lado de lá". Sem ele, acho que este livro seria muito diferente; seria mais apaixonado, porém menos verdadeiro. Se meu texto olha para o TDD de uma maneira fria e imparcial, a culpa é dele. Os srs. Paulo Silveira e Adriano Almeida também merecem uma lembrança. Mesmo na época em que a Casa do Código não existia de fato, eles já haviam aceitado a ideia do livro de TDD. Obrigado pela confiança. Todas as pessoas das últimas empresas em que atuei também me ajudaram muito com as incontáveis conversas de corredor sobre o assunto. Isso com certeza enriqueceu muito o texto. Agradeço também aos amigos José Donizetti, Guilherme Moreira e Rafael Ferreira, que gastaram tempo lendo o livro e me dando sugestões de como melhorar. Por fim, obrigado a você que está lendo este livro. Espero que ele ajude. AGRADECIMENTOS E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Impresso e PDF: 978-85-66250-04-6 EPUB: 978-85-66250-73-2 Você pode discutir sobre este livro no Fórum da Casa do Código: http://forum.casadocodigo.com.br/. Caso você deseje submeter alguma errata ou sugestão, acesse http://erratas.casadocodigo.com.br. ISBN E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Meu nome é Mauricio Aniche, e trabalho com desenvolvimento de software há 10 anos. Em boa parte desse tempo, atuei como consultor para diferentes empresas do mercado brasileiro e internacional. Com certeza, as linguagens mais utilizadas por mim ao longo da minha carreira foram Java, C# e C. Como sempre pulei de projeto em projeto (e, por consequência, de tecnologia em tecnologia), nunca fui a fundo em nenhuma delas. Pelo contrário, sempre foquei em entender princípios que pudessem ser levados de uma para outra, para que no fim, o código saísse com qualidade, independente da tecnologia. Em meu último ano da graduação, 2007, comecei a ler mais sobre a ideia de testes automatizados e TDD. Achei muito interessante e útil a ideia de se escrever um programa para testar seu programa. Então, decidi praticar TDD por conta própria, para entender melhor como ela funcionava. Gostei muito do que vi. De 2007 em diante, resolvi praticar, pesquisar e divulgar melhor minhas ideias sobre o assunto. Comecei devagar, apenas blogando o que estava na minha cabeça e sobre o que gostaria de feedback de outros desenvolvedores. Mas para fazer isso de maneira mais decente, resolvi ingressar no programa de Mestrado da Universidade de São Paulo. Lá, pesquisei sobre os efeitos da prática de TDD no design de classes. Ao longo desse tempo, participei da grande maioria dos eventos relacionados ao assunto. Palestrei nos principais eventos de métodos ágeis do país (como Agile Brazil, Encontro Ágil), de desenvolvimento de software (QCON SP e DNAD), entre outros menores. Cheguei a participar de eventos internacionais também; QUEM SOU EU? E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com fui o único palestrante brasileiro no Primeiro Workshop Internacional sobre TDD, em 2010, na cidade de Paris. Isso mostra também que tenho participado dos eventos acadêmicos. Em 2011, apresentei um estudo sobre TDD no WBMA (Workshop Brasileiro de Métodos Ágeis), e em 2012, no maior simpósio brasileiro sobre engenharia de software, o SBES. Atualmente trabalho pela Caelum, como consultor e instrutor. Também sou aluno de doutorado pela Universidade de São Paulo, onde continuo a pesquisar sobre a relação dos testes de unidade e qualidade do código. Portanto, esse é meu relacionamento com TDD. Nos últimos anos. tenho olhado-o de todos os pontos de vista possíveis: de praticante, de acadêmico, de pesquisador, de apaixonado, de frio. Este livro é o relato de tudo que aprendi nesses últimos anos. E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Já faz 15 anos desde que Test-Driven Development (TDD) evoluiu para algo que é considerado uma prática útil pela comunidade de software. Originalmente, TDD evoluiu da Programação Extrema (XP), discutida por Kent Beck e outros antes mesmo do Manifesto Ágil. Seu foco primordial era guiar o desenvolvimento de software fazendo o desenvolvedor realmente pensar sobre como ele validaria que o software estava implementado corretamente antes de se escrever o código daquele sistema. O caminho é longo para ser proficiente em TDD. Desenvolver baterias de teste automatizadas, refatoração, retrabalhar nos tests para eliminar duplicação e testar condições excepcionais são algumas delas. Eu conheço o autor (Maurício) há muitos anos. Maurício tem grande experiência em padrões, projeto de sistemas orientados a objetos, Java e TDD. Eu colaborei e trabalhei com ele em projetos e artigos de sistemas orientados a objetos e, mais específicamente, sobre TDD. Maurício tem muita experiência no desenvolvimento de sistemas reais usando os princípios de TDD. Maurício tem mais do que somente o entendimento do "como" implementar uma nova funcionalidade usando TDD. Ele entende claramente os trade-offs de um bom design orientado a objetos, quando e onde aplicar padrões para se ter um design mais limpo, reusável e fácil de mudar. Qualquer desenvolvedor que queira se tornar proficiente em TDD não precisará apenas entender os princípios básicos de se escrever um teste de unidade. Ele precisará também conhecer princípios mais avançados, como refatoração, como manter seus PREFÁCIO E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com testes limpos, TDD e como ele se relaciona com projeto de classes, minimizando acoplamento etc. Este livro é mais do que apenas um livro de receitas sobre como praticar TDD. Ele discute todos os principais detalhes de TDD. Joseph W. Yoder The Refactory, Inc. — 2015 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com TDD é uma das práticas de desenvolvimento de software sugeridas por diversas metodologias ágeis, como XP. A ideia é fazer com que o desenvolvedor escreva testes automatizados de maneira constante ao longo do desenvolvimento. Mas, diferentemente do que estamos acostumados, TDD sugere que o desenvolvedor escreva o teste antes mesmo da implementação. Essa simples inversão no ciclo traz diversos benefícios para o projeto. Baterias de testes tendem a ser maiores, cobrindo mais casos, e garantindo uma maior qualidade externa. Além disso, escrever testes de unidade forçará o desenvolvedor a escrever um código de melhor qualidade pois, como veremos ao longo do livro, para escrever bons testes de unidade, o desenvolvedor é obrigado a fazer bom uso de orientação a objetos. A prática nos ajuda a escrever um software melhor, com mais qualidade, e um código melhor, mais fácil de ser mantido e evoluído. Esses dois pontos são importantíssimosem qualquer software, e TDD nos ajuda a alcançá-los. Toda prática que ajuda a aumentar a qualidade do software produzido deve ser estudada. Neste livro, tentei colocar toda a experiência e tudo que aprendi ao longo desses últimos anos praticando e pesquisando sobre o assunto. Mostrei também o outro lado da prática, seus efeitos no design de classes, que é muito falada mas pouco discutida e explicada. A prática de TDD, quando bem usada, pode ser bastante produtiva. Mas, como verá ao longo do livro, os praticantes devem estar sempre alertas às dicas que o teste dará sobre nosso código. Aqui, passaremos por eles e o leitor ao final do livro terá em mãos RESUMO E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com uma nova e excelente ferramenta de desenvolvimento. Este livro é destinado a desenvolvedores que querem aprender a escrever testes de maneira eficiente, e que desejam melhorar ainda mais o código que produzem. Aqui utilizamos Java para demonstrar os conceitos discutidos, mas você pode facilmente levar as discussões feitas aqui para a sua linguagem de programação favorita. Mesmo que você já pratique TDD, tenho certeza de que encontrará discussões interessantes sobre como a prática dá feedback sobre problemas de acoplamento e coesão, bem como técnicas para escrever testes melhores e mais fáceis de serem mantidos. Testadores também podem se beneficiar deste livro, entendendo como escrever códigos de teste de qualidade, quando ou não usar TDD, e como reportar problemas de código para os desenvolvedores. Ao longo do livro, trabalhamos em diversos exemplos, muito similares ao mundo real. Todo capítulo possui sua parte prática e parte teórica. Na parte prática, muito código de teste é escrito. Na parte teórica, refletimos sobre o código que produzimos até aquele momento, o que foi feito de bom, o que foi feito de ruim, e melhoramos de acordo. O leitor pode refazer todos os códigos produzidos nos capítulos. Praticar TDD é essencial para que as ideias fiquem naturais. Além disso, a Caelum também disponibiliza um curso online sobre testes automatizados (http://www.caelum.com.br/curso/online/testes- automatizados), que pode ser usado como complemento deste livro. A quem se destina este livro? Como devo estudar? E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Boa leitura! E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com 1 1 2 3 3 5 7 7 8 10 17 19 20 20 21 28 29 32 34 Sumário 1 Introdução 1.1 Era uma vez um projeto sem testes... 1.2 Por que devemos testar? 1.3 Por que não testamos? 1.4 Testes automatizados e TDD 1.5 Conclusão 2 Testes de unidade 2.1 O que é um teste de unidade? 2.2 Preciso mesmo escrevê-los? 2.3 O primeiro teste de unidade 2.4 Continuando a testar 2.5 Conclusão 3 Introdução ao Test-Driven Development 3.1 O problema dos números romanos 3.2 O primeiro teste 3.3 Refletindo sobre o assunto 3.4 Quais as vantagens? 3.5 Um pouco da história de TDD 3.6 Conclusão SumárioCasa do Código E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com 36 36 38 40 47 51 53 53 58 60 62 63 65 66 69 71 73 76 79 81 82 83 83 87 90 91 95 97 4 Simplicidade e Baby Steps 4.1 O problema do cálculo de salário 4.2 Implementando da maneira mais simples possível 4.3 Passos de bebê (ou baby steps) 4.4 Usando baby steps de maneira consciente 4.5 Conclusão 5 TDD e design de classes 5.1 O problema do carrinho de compras 5.2 Testes que influenciam no design de classes 5.3 Diferenças entre TDD e testes da maneira tradicional 5.4 Testes como rascunho 5.5 Conclusão 6 Qualidade no código do teste 6.1 Repetição de código entre testes 6.2 Nomenclatura dos testes 6.3 Test Data Builders 6.4 Testes repetidos 6.5 Escrevendo boas asserções 6.6 Testando listas 6.7 Separando as classes de teste 6.8 Conclusão 7 TDD e a coesão 7.1 Novamente o problema do cálculo de salário 7.2 Ouvindo o feedback dos testes 7.3 Testes em métodos privados? 7.4 Resolvendo o problema da calculadora de salário 7.5 O que olhar no teste em relação a coesão? 7.6 Conclusão Casa do CódigoSumário E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com 98 98 102 106 107 108 112 115 117 120 121 123 123 129 130 132 133 134 135 135 137 140 145 146 149 150 150 152 8 TDD e o acoplamento 8.1 O problema da nota fiscal 8.2 Mock objects 8.3 Dependências explícitas 8.4 Ouvindo o feedback dos testes 8.5 Classes estáveis 8.6 Resolvendo o problema da nota fiscal 8.7 Testando métodos estáticos 8.8 TDD e a constante criação de interfaces 8.9 O que olhar no teste em relação ao acoplamento? 8.10 Conclusão 9 TDD e o encapsulamento 9.1 O problema do processador de boleto 9.2 Ouvindo o feedback dos testes 9.3 Tell, Don't Ask e Lei de Demeter 9.4 Resolvendo o problema do processador de boletos 9.5 O que olhar no teste em relação ao encapsulamento? 9.6 Conclusão 10 Testes de integração e TDD 10.1 Testes de unidade, integração e sistema 10.2 Quando não usar mocks? 10.3 Testes em DAOs 10.4 Devo usar TDD em testes de integração? 10.5 Testes em aplicações Web 10.6 Conclusão 11 Quando não usar TDD? 11.1 Quando não praticar TDD? 11.2 100% de cobertura de código? SumárioCasa do Código E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com 154 155 156 159 160 162 162 163 164 164 166 166 170 175 176 11.3 Devo testar códigos simples? 11.4 Erros comuns durante a prática de TDD 11.5 Como convencer seu chefe sobre TDD? 11.6 TDD em sistemas legados 11.7 Conclusão 12 E agora? 12.1 Como aprender mais agora? 12.2 Dificuldade no aprendizado 12.3 Como interagir com outros praticantes? 12.4 Conclusão final 13 Apêndice: princípios SOLID 13.1 Sintomas de projetos de classes em degradação 13.2 Princípios de projeto de classes 13.3 Conclusão 14 Bibliografia Versão: 20.0.27 Casa do CódigoSumário E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com CAPÍTULO 1 Será que testar software é realmente importante? Neste capítulo, discutimos um pouco sobre as consequências de software não testado e uma possível solução para isso, que hoje é um problema para a sociedade como um todo, já que softwares estão em todos os lugares. Durante os anos de 2005 e 2006, trabalhei em um projeto cujo objetivo era automatizar todo o processo de postos de gasolina. O sistema deveria tomar conta de todo o fluxo: desde a comunicação com as bombas de gasolina, liberando ou não o abastecimento, até relatórios de fluxo de caixa e quantidade de combustível vendido por dia ou por bomba. A aplicação era desenvolvida inteira em C e deveria rodar em um microdispositivo de 200Mhz de processamento e 2MB de RAM. Nós éramos em 4 desenvolvedores, e todos programavam e testavam ao mesmo tempo. Os testes não eram tão simples de serem feitos, afinal, precisávamos simular bombas de gasolinas, abastecimentos simultâneos etc. Mas, mesmo assim, nos revezávamos e testávamos da forma que conseguíamos. Após alguns meses de desenvolvimento, encontramos o INTRODUÇÃO 1.1 ERA UMA VEZ UM PROJETO SEM TESTES... 1 INTRODUÇÃO 1 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com primeiro posto disposto a participar do piloto da aplicação. Esse posto ficava em Santo Domingo, capital da República Dominicana. Eu, na época líder técnico dessa equipe, viajei para o lugar com o objetivo de acompanhar nosso produto rodando pela primeira vez. Fizemos a instalaçãodo produto pela manhã e acompanhamos nosso "bebê" rodando por todo o dia. Funcionou perfeitamente. Saímos de lá e fomos jantar em um dos melhores lugares da cidade para comemorar. Missão cumprida. Voltando do jantar, fui direto para cama, afinal, no dia seguinte entenderíamos quais seriam as próximas etapas do produto. Mas às 7h da manhã, o telefone tocou. Era o responsável pelo posto de gasolina piloto. Ele me ligou justamente para contar que o posto de gasolina estava completamente parado desde as 0h: o software parou de funcionar e bloqueou completamente o posto de gasolina. Nosso software nunca havia sido testado com uma quantidade grande de abastecimentos. Os postos em Santo Domingo fazem muitos pequenos abastecimentos ao longo do dia (diferente daqui, onde enchemos o tanque de uma vez). O sistema não entendeu isso muito bem, e optou por bloquear as bombas de gasolina, para evitar fraude, já que não conseguia registrar as futuras compras. O software fez com que o estabelecimento ficasse 12 horas parado, sem vender nada. Quanto será que isso custou ao dono do estabelecimento? Como será que foi a reação dele ao descobrir que o novo produto, no primeiro dia, causou tamanho estrago? Os Estados Unidos estimam que bugs de software lhes custam aproximadamente 60 bilhões de dólares por ano (THIBODEAU, 1.2 POR QUE DEVEMOS TESTAR? 2 1.2 POR QUE DEVEMOS TESTAR? E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com 2012). O dinheiro que poderia estar sendo usado para erradicar a fome do planeta está sendo gasto em correções de software que não funcionam. É incrível a quantidade de software que não funciona. Pergunte ao seu amigo não-técnico se ele já ficou irritado porque algum programa do seu dia a dia simplesmente parou de funcionar. Alguns bugs de software são inclusive famosos por todos: o foguete Ariane 5 explodiu por um erro de software; um hospital panamenho matou pacientes pois seu software para dosagem de remédios errou. Não há um desenvolvedor que não saiba que a solução para o problema é testar seus códigos. A pergunta é: por que não testamos? Não testamos, porque testar sai caro. Imagine o sistema em que você trabalha hoje. Se uma pessoa precisasse testá-lo do começo ao fim, quanto tempo ela levaria? Semanas? Meses? Pagar um mês de uma pessoa a cada mudança feita no código (sim, os desenvolvedores também sabem que uma mudança em um trecho pode gerar problemas em outro) é simplesmente impossível. Testar sai caro, no fim, porque estamos pagando "a pessoa" errada para fazer o trabalho. Acho muito interessante a quantidade de tempo que gastamos criando soluções tecnológicas para resolver problemas "dos outros". Por que não escrevemos programas que resolvam também os nossos problemas? Uma maneira para conseguir testar o sistema todo de maneira constante e contínua a um preço justo é automatizando os testes. 1.3 POR QUE NÃO TESTAMOS? 1.4 TESTES AUTOMATIZADOS E TDD 1.3 POR QUE NÃO TESTAMOS? 3 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Ou seja, escrevendo um programa que testa o seu programa. Esse programa invocaria os comportamentos do seu sistema e garantiria que a saída é sempre a esperada. Se isso fosse realmente viável, teríamos diversas vantagens. O teste executaria muito rápido (afinal, é uma máquina!). Se ele executa rápido, logo o rodaríamos constantemente. Se os rodarmos o tempo todo, descobriríamos os problemas mais cedo, diminuindo o custo que o bug geraria. Um ponto que é sempre levantado em qualquer discussão sobre testes manuais versus testes automatizados é produtividade. O argumento mais comum é o de que agora a equipe de desenvolvimento gastará tempo escrevendo código de teste; antes ela só gastava tempo escrevendo código de produção. Portanto, essa equipe será menos produtiva. A resposta para essa pergunta é: o que é produtividade? Se produtividade for medida através do número de linhas de código de produção escritos por dia, talvez o desenvolvedor seja sim menos produtivo. Agora, se produtividade for a quantidade de linhas de código de produção sem defeitos escritos por dia, provavelmente o desenvolvedor será mais produtivo ao usar testes automatizados. Além disso, se analisarmos o dia a dia de um desenvolvedor que faz testes manuais, podemos perceber a quantidade de tempo que ele gasta com teste. Geralmente, ele executa testes enquanto desenvolve o algoritmo completo. Ele escreve um pouco, roda o programa, e o programa falha. Nesse momento, o desenvolvedor entende o problema, corrige-o, e em seguida executa novamente o mesmo teste. Quantas vezes por dia ele executa o mesmo teste manual? O desenvolvedor que automatiza seus testes perde tempo apenas 1 vez com ele; nas próximas, ele simplesmente aperta um botão e vê a 4 1.4 TESTES AUTOMATIZADOS E TDD E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com máquina executando o teste pra ele, de forma correta e rápida. Minha família inteira é da área médica. Um jantar de fim de semana em casa parece mais um daqueles episódios de seriados médicos da televisão: pessoas discutindo casos e como resolvê-los. Apesar de entender praticamente nada sobre medicina, uma coisa me chama muito a atenção: o fanatismo deles por qualidade. Um médico, ao longo de uma cirurgia, nunca abre mão de qualidade. Se o paciente falar para ele: "Doutor, o senhor poderia não lavar a mão e terminar a cirurgia 15 minutos mais cedo?", tenho certeza de que o médico negaria na hora. Ele saberia que chegaria ao resultado final mais rápido, mas a chance de um problema é tão grande, que simplesmente não valeria a pena. Em nossa área, vejo justamente o contrário. Qual desenvolvedor nunca escreveu um código de má qualidade de maneira consciente? Quem nunca escreveu uma "gambiarra"? Quem nunca colocou software em produção sem executar o mínimo suficiente de testes para tal? Não há desculpas para não testar software. E a solução para que seus testes sejam sustentáveis é automatizando. Testar é divertido, aumenta a qualidade do seu produto, e pode ainda ajudá-lo a identificar trechos de código que foram mal escritos ou projetados (aqui entra a prática de TDD). É muita vantagem. Ao longo do livro, espero convencê-lo de que testar é importante, e que na verdade é mais fácil do que parece. Para facilitar a comunicação entre os leitores deste livro e 1.5 CONCLUSÃO Como tirar dúvidas? 1.5 CONCLUSÃO 5 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com interessados no assunto, existe o fórum da Casa do Código, em http://forum.casadocodigo.com.br/. Toda e qualquer discussão, dúvida ou sugestão será bem-vinda. Não se esqueça também de participar do GUJ, o maior portal de desenvolvedores do Brasil: http://www.guj.com.br/. Lá você pode perguntar, responder, editar e dar pontos às dúvidas e respostas de outros desenvolvedores. Caso você deseje submeter alguma errata ou sugestão, acesse http://erratas.casadocodigo.com.br 6 1.5 CONCLUSÃO E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com CAPÍTULO 2 Imagine-se passeando em uma loja virtual qualquer na web. Ao selecionar um produto, o sistema coloca-o no seu carrinho de compras. Ao finalizar a compra, o sistema fala com a operadora de cartão de crédito, retira o produto do estoque, dispara um evento para que a equipe de logística separeos produtos comprados e lhe envia um e-mail confirmando a compra. O software que toma conta de tudo isso é complexo. Ele contém regras de negócio relacionadas ao carrinho de compras, ao pagamento, ao fechamento da compra. Mas, muito provavelmente, todo esse código não está implementado em apenas um único arquivo; esse sistema é composto por diversas pequenas classes, cada uma com sua tarefa específica. Desenvolvedores, quando pensam em teste de software, geralmente imaginam um teste que cobre o sistema como um todo. Um teste de unidade não se preocupa com todo o sistema; ele está interessado apenas em saber se uma pequena parte do sistema funciona. Um teste de unidade testa uma única unidade do nosso sistema. Geralmente, em sistemas orientados a objetos, essa unidade é a classe. Em nosso sistema de exemplo, muito provavelmente existem classes como "CarrinhoDeCompras", "Pedido", e assim por diante. A TESTES DE UNIDADE 2.1 O QUE É UM TESTE DE UNIDADE? 2 TESTES DE UNIDADE 7 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com ideia é termos baterias de testes de unidade separadas para cada uma dessas classes; cada bateria preocupada apenas com a sua classe. Essa mesma loja virtual precisa encontrar, dentro do seu carrinho de compras, os produtos de maior e menor valor. Um possível algoritmo para esse problema seria percorrer a lista de produtos no carrinho, comparar um a um, e guardar sempre a referência para o menor e o maior produto encontrado até então. Em código, uma possível implementação seria: public class MaiorEMenor { private Produto menor; private Produto maior; public void encontra(CarrinhoDeCompras carrinho) { for(Produto produto : carrinho.getProdutos()) { if(menor == null || produto.getValor() < menor.getValor()) { menor = produto; } else if (maior == null || produto.getValor() > maior.getValor()) { maior = produto; } } } public Produto getMenor() { return menor; } public Produto getMaior() { return maior; } } Veja o método encontra() . Ele recebe um 2.2 PRECISO MESMO ESCREVÊ-LOS? 8 2.2 PRECISO MESMO ESCREVÊ-LOS? E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com CarrinhoDeCompras e percorre a lista de produtos, comparando sempre o produto corrente com o "menor e maior de todos". Ao final, temos no atributo maior e menor os produtos desejados. A figura a seguir mostra como o algoritmo funciona: Para exemplificar o uso dessa classe, veja o seguinte código: public class TestaMaiorEMenor { public static void main(String[] args) { CarrinhoDeCompras carrinho = new CarrinhoDeCompras(); carrinho.adiciona(new Produto("Liquidificador", 250.0)); carrinho.adiciona(new Produto("Geladeira", 450.0)); carrinho.adiciona(new Produto("Jogo de pratos", 70.0)); MaiorEMenor algoritmo = new MaiorEMenor(); algoritmo.encontra(carrinho); System.out.println("O menor produto: " + algoritmo.getMenor().getNome()); System.out.println("O maior produto: " + algoritmo.getMaior().getNome()); } } O carrinho contém três produtos: liquidificador, geladeira e jogo 2.2 PRECISO MESMO ESCREVÊ-LOS? 9 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com de pratos. É fácil perceber que o jogo de pratos é o produto mais barato (R$ 70,00), enquanto que a geladeira é o mais caro (R$ 450,00). A saída do programa é exatamente igual à esperada: O menor produto: Jogo de pratos O maior produto: Geladeira Apesar de aparentemente funcionar, se esse código for para produção, a loja virtual terá problemas. A classe MaiorEMenor respondeu corretamente ao teste anterior, mas ainda não é possível dizer se ela realmente funciona para outros cenários. Observe o código a seguir: public class TestaMaiorEMenor { public static void main(String[] args) { CarrinhoDeCompras carrinho = new CarrinhoDeCompras(); carrinho.adiciona(new Produto("Geladeira", 450.0)); carrinho.adiciona(new Produto("Liquidificador", 250.0)); carrinho.adiciona(new Produto("Jogo de pratos", 70.0)); MaiorEMenor algoritmo = new MaiorEMenor(); algoritmo.encontra(carrinho); System.out.println("O menor produto: " + algoritmo.getMenor().getNome()); System.out.println("O maior produto: " + algoritmo.getMaior().getNome()); } } Esse código não é tão diferente do anterior. Os produtos, bem como os valores, são os mesmos; apenas a ordem em que eles são inseridos no carrinho foi trocada. Espera-se então que o programa produza a mesma saída. Mas, ao executá-lo, a seguinte saída é gerada: O menor produto: Jogo de pratos Exception in thread "main" java.lang.NullPointerException at TestaMaiorEMenor.main(TestaMaiorEMenor.java:12) 2.3 O PRIMEIRO TESTE DE UNIDADE 10 2.3 O PRIMEIRO TESTE DE UNIDADE E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Problema! Essa não era a saída esperada! Agora está claro que a classe MaiorEMenor não funciona bem para todos os cenários. Se os produtos, por algum motivo, forem adicionados no carrinho em ordem decrescente, a classe não consegue calcular corretamente. Uma pergunta importante para o momento é: será que o desenvolvedor, ao escrever a classe, perceberia esse bug? Será que ele faria todos os testes necessários para garantir que a classe realmente funcione? Como discutido no capítulo anterior, equipes de software tendem a não testar software, pois o teste leva tempo e, por consequência, dinheiro. Na prática, equipes acabam por executar poucos testes ralos, que garantem apenas o cenário feliz e mais comum. Todos os problemas da falta de testes, que já foram discutidos anteriormente, agora fazem sentido. Imagine se a loja virtual colocasse esse código em produção. Quantas compras seriam perdidas por causa desse problema? Para diminuir a quantidade de bugs levados para o ambiente de produção, é necessário testar o código constantemente. Idealmente, a cada alteração feita, todo o sistema deve ser testado por inteiro novamente. Mas isso deve ser feito de maneira sustentável: é impraticável pedir para que seres humanos testem o sistema inteiro a cada alteração feita por um desenvolvedor. A solução para o problema é fazer com que a máquina teste o software. A máquina executará o teste rapidamente e sem custo, e o desenvolvedor não gastaria mais tempo executando testes manuais, mas sim apenas em evoluir o sistema. Escrever um teste automatizado não é tarefa tão árdua. Ele, na verdade, se parece muito com um teste manual. Imagine um desenvolvedor que deva testar o comportamento do carrinho de 2.3 O PRIMEIRO TESTE DE UNIDADE 11 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com compras da loja virtual quando existem dois produtos lá cadastrados: primeiramente, ele "clicaria em comprar em dois produtos", em seguida "iria para o carrinho de compras", e por fim, verificaria "a quantidade de itens no carrinho (deve ser 2)" e o "o valor total do carrinho (que deve ser a soma dos dois produtos adicionados anteriormente)". Ou seja, de forma generalizada, o desenvolvedor primeiro pensa em um cenário (dois produtos comprados), depois executa uma ação (vaiao carrinho de compras), e por fim, valida a saída (vê a quantidade de itens e o valor total do carrinho). A figura a seguir mostra os passos que um testador geralmente faz quando deseja testar uma funcionalidade. Um teste automatizado é similar. Ele descreve um cenário, executa uma ação e valida uma saída. A diferença é que quem fará tudo isso será a máquina, sem qualquer intervenção humana. De certa forma, um teste automatizado já foi escrito neste capítulo. Veja o código adiante, escrito para testar superficialmente a classe MaiorEMenor : public class TestaMaiorEMenor { public static void main(String[] args) { CarrinhoDeCompras carrinho = new CarrinhoDeCompras(); carrinho.adiciona(new Produto("Geladeira", 450.0)); carrinho.adiciona(new Produto("Liquidificador", 250.0)); carrinho.adiciona(new Produto("Jogo de pratos", 70.0)); MaiorEMenor algoritmo = new MaiorEMenor(); algoritmo.encontra(carrinho); System.out.println("O menor produto: " + algoritmo.getMenor().getNome()); 12 2.3 O PRIMEIRO TESTE DE UNIDADE E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com System.out.println("O maior produto: " + algoritmo.getMaior().getNome()); } } Veja que esse código contém, de forma automatizada, boa parte do que foi descrito em relação ao teste manual: ele monta um cenário (um carrinho de compras com 3 produtos), executa uma ação (invoca o método encontra() ), e valida a saída (imprime o maior e o menor produto). E o melhor: uma máquina faz (quase) tudo isso. Ao rodar o código anterior, a máquina monta o cenário e executa a ação sem qualquer intervenção humana. Mas uma ação humana ainda é requerida para descobrir se o comportamento executou de acordo. Um humano precisa ver a saída e conferir com o resultado esperado. É preciso que o próprio teste faça a validação e informe o desenvolvedor caso o resultado não seja o esperado. Para melhorar esse código, agora só introduzindo um framework de teste automatizado. Esse framework nos daria um relatório mais detalhado dos testes que foram executados com sucesso e, mais importante, dos testes que falharam, trazendo o nome do teste e a linha que apresentaram problemas. Neste livro, faremos uso do JUnit (http://www.junit.org), o framework de testes de unidade mais popular do mundo Java. O JUnit possui um plugin para Eclipse, que mostra uma lista de todos os testes executados, pintando-os de verde em caso de sucesso, ou vermelho em caso de falha. Ao clicar em um teste vermelho, a ferramenta ainda apresenta a linha que falhou, o resultado que era esperado e o resultado devolvido pelo método. Para converter o código anterior em um teste que o JUnit entenda, não é necessário muito trabalho. A única parte que ainda 2.3 O PRIMEIRO TESTE DE UNIDADE 13 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com não é feita 100% pela máquina é a terceira parte do teste: a validação. É justamente ali que invocaremos os métodos do JUnit. Eles que farão a comparação do resultado esperado com o calculado. No JUnit, o método para isso é o Assert.assertEquals() : import org.junit.Assert; import org.junit.Test; public class TestaMaiorEMenor { @Test public void ordemDecrescente() { CarrinhoDeCompras carrinho = new CarrinhoDeCompras(); carrinho.adiciona(new Produto("Geladeira", 450.0)); carrinho.adiciona(new Produto("Liquidificador", 250.0)); carrinho.adiciona(new Produto("Jogo de pratos", 70.0)); MaiorEMenor algoritmo = new MaiorEMenor(); algoritmo.encontra(carrinho); Assert.assertEquals("Jogo de pratos", algoritmo.getMenor().getNome()); Assert.assertEquals("Geladeira", algoritmo.getMaior().getNome()); } } Pronto. Esse código é executado pelo JUnit. O teste, como bem sabemos, falhará: 14 2.3 O PRIMEIRO TESTE DE UNIDADE E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Para fazer o teste passar, é necessário corrigir o bug na classe de produção. O que faz o código falhar é justamente a presença do else (ele fazia com que o código nunca passasse pelo segundo if , que verificava justamente o maior elemento). Ao removê-lo, o teste passa: public class MaiorEMenor { private Produto menor; private Produto maior; public void encontra(CarrinhoDeCompras carrinho) { for(Produto produto : carrinho.getProdutos()) { if(menor == null || produto.getValor() < menor.getValor()) { menor = produto; } if (maior == null || produto.getValor() > maior.getValor()){ maior = produto; } } } public Produto getMenor() { return menor; } public Produto getMaior() { return maior; } } 2.3 O PRIMEIRO TESTE DE UNIDADE 15 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Repare também no tempo que a máquina levou para executar o teste: 0.007 segundos. Mais rápido do que qualquer ser humano faria. Isso possibilita ao desenvolvedor executar esse teste diversas vezes ao longo do seu dia de trabalho. E o melhor: se algum dia um outro desenvolvedor alterar esse código, ele poderá executar a bateria de testes automatizados existente e descobrir se a sua alteração fez alguma funcionalidade que já funcionava anteriormente parar de funcionar. Isso é conhecido como testes de regressão. Eles garantem que o sistema não regrediu. MEU PRIMEIRO TESTE AUTOMATIZADO Há muitos anos atrás, trabalhava em uma pequena consultoria de desenvolvimento de software em São Paulo. E como em toda consultoria, precisávamos anotar nossas horas de trabalho em cada um dos projetos da empresa, a fim de cobrar corretamente cada um dos clientes. Resolvi, por conta própria, criar um simples projeto para controle de horas de trabalho (eu precisava de um motivo para experimentar todas as coisas que estava aprendendo naquele momento e, dentre elas, testes automatizados). O sistema era bem simples: cadastro de funcionários, cadastro de projetos, ligação entre um funcionário e um projeto e cadastro de horas em um projeto. Ao final da implementação, apresentei o sistema ao diretor da empresa. Ele adorou, e me sugeriu a implementação de uma simples regra de negócio: se o número total de horas colocadas no projeto ultrapasse um determinado limite, nenhum funcionário poderia adicionar mais horas nele. A implementação era bem trivial: bastava fazer um if . 16 2.3 O PRIMEIRO TESTE DE UNIDADE E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Lembro-me de que implementei a regra de negócio e então escrevi o teste de unidade para garantir que havia implementado corretamente. O teste que acabei de escrever ficou verde, mas três ou quatro testes que já existiam ficaram vermelhos. A única linha de código que escrevi fez com que funcionalidades que já funcionavam parassem de trabalhar corretamente. Naquele momento, me veio a cabeça todas as vezes que escrevi uma ou duas linhas de código, aparentemente simples, mas que poderiam ter quebrado o sistema. Percebi então a grande vantagem da bateria de testes automatizados: segurança. Percebi como é fácil quebrar código, e como é difícil perceberisso sem testes automatizados. Daquele dia em diante, penso duas vezes antes de escrever código sem teste. Ainda são necessários muitos testes para garantir que a classe MaiorEMenor funcione corretamente. Até agora, o único cenário testado são produtos em ordem decrescente. Muitos outros cenários precisam ser testados. Dentre eles: Produtos com valores em ordem crescente; Produtos com valores em ordem variada; Um único produto no carrinho. Conhecendo o código da classe MaiorEMenor , é possível prever que esses cenários funcionarão corretamente. Escrever testes automatizados para eles pode parecer inútil por enquanto. Entretanto, é importante lembrar de que, em códigos reais, muitos desenvolvedores fazem alterações nele. Para o desenvolvedor 2.4 CONTINUANDO A TESTAR 2.4 CONTINUANDO A TESTAR 17 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com que implementa a classe, o código dela é bem natural e simples de ser entendido. Mas para os futuros desenvolvedores que a alterarão, esse código pode não parecer tão simples. É necessário então prover segurança para eles nesse momento. Portanto, apesar de alguns testes parecerem desnecessários nesse instante, eles garantirão que a evolução dessa classe será feita com qualidade. Apenas para exemplificar mais ainda, vamos escrever mais um teste, dessa vez para um carrinho com apenas 1 produto. Repare que, nesse cenário, é esperado que o maior e menor produtos sejam o mesmo: import org.junit.Assert; import org.junit.Test; public class TestaMaiorEMenor { @Test public void apenasUmProduto() { CarrinhoDeCompras carrinho = new CarrinhoDeCompras(); carrinho.adiciona(new Produto("Geladeira", 450.0)); MaiorEMenor algoritmo = new MaiorEMenor(); algoritmo.encontra(carrinho); Assert.assertEquals("Geladeira", algoritmo.getMenor().getNome()); Assert.assertEquals("Geladeira", algoritmo.getMaior().getNome()); } } 18 2.4 CONTINUANDO A TESTAR E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com COMPARANDO SÓ O NOME? Em vez de comparar apenas o nome do produto, você poderia comparar o objeto inteiro: Assert.assertEquals(produto, algoritmo.getMenor()); , por exemplo. Mas, para isso, o método equals() deve estar implementado na classe Produto . Em certos casos, essa solução pode ser mais interessante. Afinal, você comparará o objeto inteiro e não só apenas um atributo. Desenvolvedores gastam toda sua vida automatizando processos de outras áreas de negócio, criando sistemas para RHs, controle financeiro, entre outros, com o intuito de facilitar a vida daqueles profissionais. Por que não criar software que automatize o seu próprio ciclo de trabalho? Testes automatizados são fundamentais para um desenvolvimento de qualidade, e é obrigação de todo desenvolvedor escrevê-los. Sua existência traz diversos benefícios para o software, como o aumento da qualidade e a diminuição de bugs em produção. Nos próximos capítulos, discutiremos sobre como aumentar ainda mais o feedback que os testes nos dão sobre a qualidade do software. 2.5 CONCLUSÃO 2.5 CONCLUSÃO 19 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com CAPÍTULO 3 Desenvolvedores (ou qualquer outro papel que é executado dentro de uma equipe de software) estão muito acostumados com o "processo tradicional" de desenvolvimento: primeiro a implementação e depois o teste. Entretanto, uma pergunta interessante é: será que é possível inverter, ou seja, testar primeiro e depois implementar? E mais importante, faz algum sentido? Para responder essa pergunta, ao longo deste capítulo desenvolveremos um simples algoritmo matemático, mas dessa vez invertendo o ciclo de desenvolvimento: o teste será escrito antes da implementação. Ao final, discutiremos as vantagens e desvantagens dessa abordagem. Numerais romanos foram criados na Roma Antiga e eles foram utilizados em todo o seu império. Os números eram representados por sete diferentes símbolos, listados na tabela a seguir. I, unus, 1, (um) V, quinque, 5 (cinco) X, decem, 10 (dez) L, quinquaginta, 50 (cinquenta) C, centum, 100 (cem) INTRODUÇÃO AO TEST- DRIVEN DEVELOPMENT 3.1 O PROBLEMA DOS NÚMEROS ROMANOS 20 3 INTRODUÇÃO AO TEST-DRIVEN DEVELOPMENT E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com D, quingenti, 500 (quinhentos) M, mille, 1.000 (mil) Para representar outros números, os romanos combinavam estes símbolos, começando do algarismo de maior valor e seguindo a regra: Algarismos de menor ou igual valor à direita são somados ao algarismo de maior valor; Algarismos de menor valor à esquerda são subtraídos do algarismo de maior valor. Por exemplo, XV representa 15 (10 + 5) e o número XXVIII representa 28 (10 + 10 + 5 + 1 + 1 + 1). Há ainda uma outra regra: nenhum símbolo pode ser repetido lado a lado por mais de 3 vezes. Por exemplo, o número 4 é representado pelo número IV (5 - 1) e não pelo número IIII. Existem outras regras (especialmente para números maiores, que podem ser lidas aqui http://pt.wikipedia.org/wiki/Numera%C3%A7%C3%A3o_romana), mas em linhas gerais, este é o problema a ser resolvido. Dado um numeral romano, o programa deve convertê-lo para o número inteiro correspondente. Conhecendo o problema dos numerais romanos, é possível levantar os diferentes cenários que precisam ser aceitos pelo algoritmo: um símbolo, dois símbolos iguais, três símbolos iguais, dois símbolos diferentes do maior para o menor, quatro símbolos dois a dois, e assim por diante. Dado todos estes cenários, uns mais simples que os outros, começaremos pelo mais simples: um único símbolo. 3.2 O PRIMEIRO TESTE 3.2 O PRIMEIRO TESTE 21 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Começando pelo teste "deve entender o símbolo I". A classe responsável pela conversão pode ser chamada, por exemplo, de ConversorDeNumeroRomano , e o método converte() , recebendo uma String com o numeral romano e devolvendo o valor inteiro representado por aquele número: public class ConversorDeNumeroRomanoTest { @Test public void deveEntenderOSimboloI() { ConversorDeNumeroRomano romano = new ConversorDeNumeroRomano(); int numero = romano.converte("I"); assertEquals(1, numero); } } IMPORT ESTÁTICO Repare que aqui utilizamos assertEquals() diretamente. Isso é possível graças ao import estático do Java 5.0. Basta importar import static org.junit.Assert.* e todos os métodos estáticos dessa classe estarão disponíveis sem a necessidade de colocar o nome dela. Veja que nesse momento esse código não compila; a classe ConversorDeNumeroRomano , bem como o método converte() , não existem. Para resolver o erro de compilação, é necessário criar a classe, mesmo que sem uma implementação real: public class ConversorDeNumeroRomano { public int converte(String numeroEmRomano) { return 0; } } 22 3.2 O PRIMEIRO TESTE E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com De volta ao teste, ele agora compila. Ao executá-lo, o teste falha. Mas não há problema; isso já era esperado. Para fazê-lo passar, introduziremos ainda uma segunda regra: o código escrito deve ser sempre o mais simples possível. Com essa regra em mente, o código mais simples que fará o teste passar é fazer simplesmente o método converte() retornar o número1: public int converte(String numeroEmRomano) { return 1; } Desenvolvedores, muito provavelmente, não ficarão felizes com essa implementação, afinal ela funciona apenas para um caso. Mas isso não é problema, já que a implementação não está pronta; ainda estamos trabalhando nela. Um próximo cenário seria o símbolo V. Nesse caso, o algoritmo deve retornar 5. Novamente começando pelo teste: @Test public void deveEntenderOSimboloV() { ConversorDeNumeroRomano romano = new ConversorDeNumeroRomano(); int numero = romano.converte("V"); assertEquals(5, numero); } Esse teste também falha. Novamente faremos a implementação mais simples que resolverá o problema. Podemos, por exemplo, fazer com que o método converte() verifique o conteúdo do número a ser convertido: se o valor for "I", o método retorna 1; se o valor for "V", o método retorna 5: public int converte(String numeroEmRomano) { if(numeroEmRomano.equals("I")) return 1; else if(numeroEmRomano.equals("V")) return 5; return 0; } 3.2 O PRIMEIRO TESTE 23 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Os testes passam (os dois que temos). Poderíamos repetir o mesmo teste e a mesma implementação para os símbolos que faltam (X, L, C, M, ...). Mas nesse momento, já temos uma primeira definição sobre nosso algoritmo: quando o numeral romano possui apenas um símbolo, basta devolvermos o inteiro associado a ele. Em vez de escrever um monte de if s para cada símbolo, é possível usar um switch que corresponde melhor ao nosso cenário. Melhorando ainda mais, em vez do switch, é possível guardar os símbolos em uma tabela, ou no Java, em um mapa entre o algarismo e o inteiro correspondente a ele. Sabendo disso, vamos nesse momento alterar o código para refletir a solução: public class ConversorDeNumeroRomano { private static Map<String, Integer> tabela = new HashMap<String, Integer>() {{ put("I", 1); put("V", 5); put("X", 10); put("L", 50); put("C", 100); put("D", 500); put("M", 1000); }}; public int converte(String numeroEmRomano) { return tabela.get(numeroEmRomano); } } Ambos os testes continuam passando. Passaremos agora para um segundo cenário: dois símbolos em sequência, como por exemplo, "II" ou "XX". Começando novamente pelo teste, temos: @Test public void deveEntenderDoisSimbolosComoII() { ConversorDeNumeroRomano romano = new ConversorDeNumeroRomano(); int numero = romano.converte("II"); assertEquals(2, numero); 24 3.2 O PRIMEIRO TESTE E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com } Para fazer o teste passar de maneira simples, é possível simplesmente adicionar os símbolos "II" na tabela: private static Map<String, Integer> tabela = new HashMap<String, Integer>() {{ put("I", 1); put("II", 2); put("V", 5); put("X", 10); put("L", 50); put("C", 100); put("D", 500); put("M", 1000); }}; O teste passa. Mas, apesar de simples, essa não parece uma boa ideia de implementação: seria necessário incluir todos os possíveis símbolos nessa tabela, o que não faz sentido. É hora de refatorar esse código novamente. Uma possível solução seria iterar em cada um dos símbolos no numeral romano e acumular seu valor; ao final, retorna o valor acumulado. Para isso, é necessário mudar a tabela para guardar somente os símbolos principais da numeração romana. Uma possível implementação deste algoritmo seria: private static Map<Character, Integer> tabela = new HashMap<Character, Integer>() {{ put('I', 1); put('V', 5); put('X', 10); put('L', 50); put('C', 100); put('D', 500); put('M', 1000); }}; public int converte(String numeroEmRomano) { int acumulador = 0; for(int i = 0; i < numeroEmRomano.length(); i++) { 3.2 O PRIMEIRO TESTE 25 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com acumulador += tabela.get(numeroEmRomano.charAt(i)); } return acumulador; } DE STRING PARA CHAR? Repare que o tipo da chave no mapa mudou de String para Character. Como o algoritmo captura letra por letra da variável numeroEmRomano , usando o método charAt() , que devolve um char, a busca do símbolo fica mais direta. Os três testes continuam passando. E, dessa forma, resolvemos o problema de dois símbolos iguais em seguida. O próximo cenário são quatro símbolos, dois a dois, como por exemplo, "XXII", que deve resultar em 22. Veja o teste: @Test public void deveEntenderQuatroSimbolosDoisADoisComoXXII() { ConversorDeNumeroRomano romano = new ConversorDeNumeroRomano(); int numero = romano.converte("XXII"); assertEquals(22, numero); } Esse teste já passa sem que precisemos fazer qualquer alteração. O algoritmo existente até aqui já resolve o problema. Vamos então para o próximo cenário: números como "IV" ou "IX", em que não basta apenas somar os símbolos existentes. Novamente, começando pelo teste: @Test public void deveEntenderNumerosComoIX() { ConversorDeNumeroRomano romano = new ConversorDeNumeroRomano(); int numero = romano.converte("IX"); assertEquals(9, numero); } 26 3.2 O PRIMEIRO TESTE E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Para fazer esse teste passar, é necessário pensar um pouco melhor sobre o problema. Repare que os símbolos em um numeral romano, da direita para a esquerda, sempre crescem. Quando um número a esquerda é menor do que seu vizinho a direita, esse número deve então ser subtraído em vez de somado no acumulador. Esse algoritmo, em código: public int converte(String numeroEmRomano) { int acumulador = 0; int ultimoVizinhoDaDireita = 0; for(int i = numeroEmRomano.length() - 1; i >= 0; i--) { // pega o inteiro referente ao simbolo atual int atual = tabela.get(numeroEmRomano.charAt(i)); // se o da direita for menor, o multiplicaremos // por -1 para torná-lo negativo int multiplicador = 1; if(atual < ultimoVizinhoDaDireita) multiplicador = -1; acumulador += tabela.get(numeroEmRomano.charAt(i)) * multiplicador; // atualiza o vizinho da direita ultimoVizinhoDaDireita = atual; } return acumulador; } Os testes agora passam. Mas veja, existe código repetido ali. Na linha em que o acumulador é somado com o valor atual, tabela.get(numeroEmRomano.charAt(i)) pode ser substituído pela variável atual . Melhorando o código, temos: acumulador += atual * multiplicador; A refatoração foi feita com sucesso, já que os testes continuam passando. Ao próximo cenário: numeral romano que contenha tanto números "invertidos", como "IV", e dois símbolos lado a lado, como "XX". Por exemplo, o número "XXIV", que representa o número 24. 3.2 O PRIMEIRO TESTE 27 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Começando pelo teste: @Test public void deveEntenderNumerosComplexosComoXXIV() { ConversorDeNumeroRomano romano = new ConversorDeNumeroRomano(); int numero = romano.converte("XXIV"); assertEquals(24, numero); } Esse teste já passa; o algoritmo criado até então já atende o cenário do teste. Ainda há outros cenários que poderiam ser testados, mas já fizemos o suficiente para discutir sobre o assunto. De maneira mais abstrata, o ciclo que foi repetido ao longo do processo de desenvolvimento da classe anterior foi: Escrevemos um testede unidade para uma nova funcionalidade; Vimos o teste falhar; Implementamos o código mais simples para resolver o problema; Vimos o teste passar; Melhoramos (refatoramos) nosso código quando necessário. Esse ciclo de desenvolvimento é conhecido por Test-Driven Development (TDD), ou, Desenvolvimento Guiado pelos Testes. A ideia é simples: o desenvolvedor deve começar a implementação pelo teste, e deve o tempo todo fazer de tudo para que seu código fique simples e com qualidade. 3.3 REFLETINDO SOBRE O ASSUNTO 28 3.3 REFLETINDO SOBRE O ASSUNTO E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Esse ciclo é também conhecido como ciclo vermelho-verde- refatora (ou red-green-refactor). O primeiro passo é escrever um teste que falha. A cor vermelha representa esse teste falhando. Em seguida, o fazemos passar (a cor verde representa ele passando). Por fim, refatoramos para melhorar o código que escrevemos. Muitos praticantes de TDD afirmam que executar esse ciclo pode ser muito vantajoso para o processo de desenvolvimento. Algumas delas: Foco no teste e não na implementação — Ao começar pelo teste, o programador consegue pensar somente no que a classe deve fazer, e esquece por um momento da implementação. Isso o ajuda a pensar em melhores cenários de teste para a classe sob desenvolvimento. Código nasce testado — Se o programador pratica o ciclo corretamente, isso então implica em que todo o código de produção escrito possui ao menos um teste 3.4 QUAIS AS VANTAGENS? 3.4 QUAIS AS VANTAGENS? 29 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com de unidade verificando que ele funciona corretamente. Simplicidade — Ao buscar pelo código mais simples constantemente, o desenvolvedor acaba por fugir de soluções complexas, comuns em todos os sistemas. O praticante de TDD escreve código que apenas resolve os problemas que estão representados por um teste de unidade. Quantas vezes o desenvolvedor não escreve código desnecessariamente complexo? Melhor reflexão sobre o design da classe — No cenário tradicional, muitas vezes a falta de coesão ou o excesso de acoplamento é causado muitas vezes pelo desenvolvedor que só pensa na implementação e acaba esquecendo como a classe vai funcionar perante o todo. Ao começar pelo teste, o desenvolvedor pensa sobre como sua classe deverá se comportar frente às outras classes do sistema. O teste atua como o primeiro cliente da classe que está sendo escrita. Nele, o desenvolvedor toma decisões como o nome da classe, os seus métodos, parâmetros, tipos de retorno etc. No fim, todas elas são decisões de design e, quando o desenvolvedor consegue observar com atenção o código do teste, seu design de classes pode crescer muito em qualidade. Todos estes pontos serão mais bem descritos e aprofundados no capítulo a seguir. Nesse momento, foque nestas vantagens. Uma pergunta que pode vir a cabeça é: "No modelo tradicional, onde os testes são escritos depois, o desenvolvedor não tem os mesmos benefícios?" A resposta é sim. Mesmo desenvolvedores que escrevem testes depois podem obter as mesmas vantagens. Um desenvolvedor pode perceber que está com dificuldades de escrever um teste e descobrir 30 3.4 QUAIS AS VANTAGENS? E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com que o design da classe que implementou tem problemas; ele pode também conseguir abstrair a implementação e escrever bons cenários de teste. Mas há uma diferença crucial: a quantidade de vezes que um programador praticante de TDD recebe feedback sobre esses pontos e a quantidade que um programador que não pratica TDD recebe. Veja a figura a seguir. O praticante de TDD escreve um pouco de testes, um pouco de implementação e recebe feedback. Isso acontece ao longo do desenvolvimento de maneira frequente. Já um programador que não pratica TDD espera um tempo (às vezes longo demais) para obter o mesmo feedback. Ao receber feedback desde cedo, o programador pode melhorar o código e corrigir problemas a um custo menor do que o programador que recebeu a mesma mensagem muito tempo depois. Todo programador sabe que alterar o código que ele acabou de escrever é muito mais fácil e rápido do que alterar o código escrito 3 dias atrás. No fim, TDD apenas maximiza a quantidade de feedback sobre o código que está sendo produzido, fazendo o programador perceber os problemas antecipadamente e, por consequência, diminuindo os custos de manutenção e melhorando o código. 3.4 QUAIS AS VANTAGENS? 31 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com DESENVOLVEDORES E A BUSCA PELA COMPLEXIDADE Acho impressionante como nós, desenvolvedores, somos atraídos por complexidade. Gostamos de problemas difíceis, que nos desafiem. Lembro-me de que, no começo de carreira, quando pegava alguma funcionalidade simples de ser implementada, eu mesmo "aumentava a funcionalidade" somente para torná-la interessante do ponto de vista técnico. Ou seja, um simples relatório se tornava um relatório complexo, com diversos filtros e que podia ser exportado para muitos formatos diferentes. Aprender a ser simples foi difícil. O TDD, de certa forma, me ajudou nisso. Ao pensar de pouco em pouco, e implementar somente o necessário para resolver o problema naquele momento, comecei a perceber que poderia gastar meu tempo implementando funcionalidades que realmente agregariam valor para o usuário final. Portanto, não ache que "ser simples" é fácil. Lembre-se de que somos atraídos por complexidade. TDD ficou bastante popular após a publicação do livro TDD: By Example, do Kent Beck, em 2002. O próprio Kent afirma que TDD não foi uma ideia totalmente original. Ele conta que, em algum momento de sua vida, leu em algum dos livros de seu pai (que também era programador) sobre uma técnica de testes mais antiga, com a qual o programador colocava na fita o valor que ele esperava daquele programa, e então o desenvolvia até chegar naquele valor. 3.5 UM POUCO DA HISTÓRIA DE TDD 32 3.5 UM POUCO DA HISTÓRIA DE TDD E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Ele próprio conta que achou a ideia estúpida. Qual o sentido de escrever um teste que falha? Mas resolveu experimentar. Após a experiência, ele disse que "as pessoas sempre falavam para ele conseguir separar o que o programa deve fazer da sua implementação final, mas que ele não sabia como fazer, até aquele momento em que resolveu escrever o teste antes." Daquele momento em diante, Kent Beck continuou a trabalhar na ideia. Em 1994, ele escreveu o seu primeiro framework de testes de unidade, o SUnit (para Smalltalk). Em 1995, ele apresentou TDD pela primeira vez na OOPSLA (conferência muito famosa da área de computação, já que muitas novidades tendem a aparecer lá). Já em 2000, o JUnit surgiu e Kent Beck, junto com Erich Gamma, publicou o artigo chamado Test Infected (2000), que mostrava as vantagens de se ter testes automatizados e como isso pode ser viciante. Finalmente em 2002, Kent Beck lançou seu livro sobre isso, e desde então a prática tem se tornado cada vez mais popular entre os desenvolvedores. A história mais completa pode ser vista na Wiki da C2 (http://c2.com/cgi/wiki?TenYearsOfTestDrivenDevelopment) ou na palestra do Steve Freeman e Michael Feathers na QCON de 2009 (FEATHERS; FREEMAN, 2009). 3.5 UM POUCO DA HISTÓRIA DE TDD 33 E-book gerado especialmente paraHenrique Pimentel - henriq.pimentel@gmail.com FERREIRA FALA Duas histórias muito conhecidas cercam Kent Beck e o nascimento dos primeiros frameworks de testes unitários. O SUnit, para Smalltalk, não era um código distribuído em arquivos da maneira usual. Beck na época atuava como consultor e tinha o hábito de recriar o framework junto com os programadores de cada cliente. A criação do JUnit também é legendária: a primeira versão foi desenvolvida numa sessão de programação pareada entre o Kent Beck e o Erich Gamma em um voo Zurique-Atlanta. Neste capítulo, apresentamos TDD e mostramos algumas das vantagens que o programador obtém ao utilizá-la no seu dia a dia. Entretanto, é possível melhorar ainda mais a maneira na qual o programador faz uso da prática. Somente praticando TDD com esse simples exemplo, é possível levantar diversas questões, como: Muitos cenários foram deixados para trás, como por exemplo, a conversão de números como "CC", "MM" etc. Eles devem ser escritos? O inverso da pergunta anterior: testes para "I", "V", "X" devem ser considerados diferentes ou repetidos? E quanto à repetição de código dentro de cada teste? É possível melhorar a qualidade de código do próprio teste? 3.6 CONCLUSÃO 34 3.6 CONCLUSÃO E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com A ideia de sempre fazer o teste passar da maneira mais simples possível faz sentido? Deve ser aplicado 100% do tempo? Existe alguma regra para dar nomes aos testes? Qual o momento ideal para refatorar o código? Se durante a implementação, um teste antigo, que passava, quebra, o que devo fazer? Para que um desenvolvedor faça uso de TDD de maneira profissional e produtiva, estes e outros pontos devem ser discutidos e clarificados. Nos próximos capítulos, responderemos a cada uma dessas perguntas, usando como exemplo um sistema razoavelmente complexo muito similar aos encontrados no mundo real. 3.6 CONCLUSÃO 35 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com CAPÍTULO 4 No capítulo anterior, apresentamos TDD em um exemplo complexo do ponto de vista algorítmico, mas bem controlado. Mesmo assim, ele foi muito útil e serviu para mostrar os passos de um desenvolvedor que pratica TDD. O mundo real é mais complexo do que isso. Algoritmos matemáticos são combinados e interagem com entidades que vêm do banco de dados e são apresentados para o usuário através de alguma interface. De agora em diante, os exemplos a serem trabalhados serão outros e, com certeza, mais parecidos com a realidade. No mundo real, as coisas evoluem e mudam rapidamente. Portanto, nos próximos exemplos, sempre imagine que eles ficarão mais complexos e evoluirão. Suponha que uma empresa precise calcular o salário do funcionário e seus descontos. Para calcular esse desconto, a empresa leva em consideração o salário atual e o cargo do funcionário. Vamos representar funcionários e cargos da seguinte maneira: public enum Cargo { DESENVOLVEDOR, SIMPLICIDADE E BABY STEPS 4.1 O PROBLEMA DO CÁLCULO DE SALÁRIO 36 4 SIMPLICIDADE E BABY STEPS E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com DBA, TESTADOR } public class Funcionario { private String nome; private double salario; private Cargo cargo; public Funcionario(String nome, double salario, Cargo cargo) { this.nome = nome; this.salario = salario; this.cargo = cargo; } public String getNome() { return nome; } public double getSalario() { return salario; } public Cargo getCargo() { return cargo; } } Repare que um funcionário possui nome, salário e cargo. O cargo é representado por uma enumeração. Nesse momento, a enumeração contém apenas desenvolvedor, testador e DBA. Em uma situação real, essa enumeração seria maior ainda, mas com esses 3 já gera uma boa discussão. As regras de negócio são as seguintes: Desenvolvedores possuem 20% de desconto caso seus salários sejam maiores do que R$ 3000,0. Caso contrário, o desconto é de 10%. DBAs e testadores possuem desconto de 25% se seus 4.1 O PROBLEMA DO CÁLCULO DE SALÁRIO 37 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com salários forem maiores do que R$ 2500,0. Em caso contrário, 15%. É hora de implementar essas regras. Uma primeira ideia é criar uma classe CalculadoraDeSalario , que recebe um Funcionario e retorna o salário do funcionário com o desconto já subtraído. O primeiro cenário a ser testado será o de desenvolvedores com salários menores do que R$ 3.000,00. Sabemos que o desconto é de 10%. Portanto, se o desenvolvedor ganhar R$ 1.500,00, seu salário menos desconto deve ser de R$ 1350,00 (1500 * 90%): public class CalculadoraDeSalarioTest { @Test public void deveCalcularSalarioParaDesenvolvedoresComSalarioAbaixoDoLimite() { CalculadoraDeSalario calculadora = new CalculadoraDeSalario(); Funcionario desenvolvedor = new Funcionario ("Mauricio", 1500.0, Cargo.DESENVOLVEDOR); double salario = calculadora.calculaSalario(desenvolvedor); assertEquals(1500.0 * 0.9, salario, 0.00001); } } Hora de fazer o teste passar. Mas lembre-se de que a ideia é fazer o teste passar da maneira mais simples possível. Veja o código a seguir: public class CalculadoraDeSalario { 4.2 IMPLEMENTANDO DA MANEIRA MAIS SIMPLES POSSÍVEL 38 4.2 IMPLEMENTANDO DA MANEIRA MAIS SIMPLES POSSÍVEL E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com public double calculaSalario(Funcionario funcionario) { return 1350.0; } } É possível ser mais simples do que isso? O código retorna diretamente o valor esperado pelo teste! Isso é aceitável, pois o código ainda não está finalizado. Vamos agora ao próximo cenário: desenvolvedores que ganham mais do que R$ 3.000.0. O teste é bem similar ao anterior: @Test public void deveCalcularSalarioParaDesenvolvedoresComSalarioAcimaDoLimite(){ CalculadoraDeSalario calculadora = new CalculadoraDeSalario(); Funcionario desenvolvedor = new Funcionario ("Mauricio", 4000.0, Cargo.DESENVOLVEDOR); double salario = calculadora.calculaSalario(desenvolvedor); assertEquals(4000.0 * 0.8, salario, 0.00001); } Novamente, fazemos o teste passar da maneira mais simples possível. Veja o código: public double calculaSalario(Funcionario funcionario) { if(funcionario.getSalario() > 3000) return 3200.0; return 1350.0; } Um simples if que verifica o salário do funcionário e retorna os valores esperados. O próximo teste agora garante que DBAs com salários inferior a R$ 1.500,00 recebam 15% de desconto: @Test public void deveCalcularSalarioParaDBAsComSalarioAbaixoDoLimite(){ CalculadoraDeSalario calculadora = new CalculadoraDeSalario(); Funcionario dba = new Funcionario 4.2 IMPLEMENTANDO DA MANEIRA MAIS SIMPLES POSSÍVEL 39 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com ("Mauricio", 500.0, Cargo.DBA); double salario = calculadora.calculaSalario(dba); assertEquals(500.0 * 0.85, salario, 0.00001); } Para fazer esse teste passar da maneira mais simples, basta novamente colocar um outro if : public double calculaSalario(Funcionario funcionario) { if(funcionario.getCargo().equals(Cargo.DESENVOLVEDOR)) { if(funcionario.getSalario() > 3000) return 3200.0; return 1350.0;} return 425.0; } Repare que, se for desenvolvedor, continuamos usando aquele código simples que escrevemos. Caso contrário, retornamos 425.0, que é o valor esperado para o salário do DBA. Mas, antes de continuarmos, precisamos discutir sobre o processo utilizado até agora. A ideia de sempre tomar o passo mais simples que resolva o problema naquele momento (e faça o teste passar) é conhecido pelos praticantes de TDD como baby steps. A vantagem desses passos de bebê é tentar levar o desenvolvedor sempre ao código mais simples e, por consequência, mais fácil de ser compreendido e mantido posteriormente. O problema é que a ideia dos passos de bebê é muito mal interpretada por muitos praticantes. Por exemplo, veja que, ao final, tínhamos 3 testes: 2 para as regras do desenvolvedor e 1 para a regra do DBA. E a implementação final gerada até então era: public double calculaSalario(Funcionario funcionario) { 4.3 PASSOS DE BEBÊ (OU BABY STEPS) 40 4.3 PASSOS DE BEBÊ (OU BABY STEPS) E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com if(funcionario.getCargo().equals(Cargo.DESENVOLVEDOR)) { if(funcionario.getSalario() > 3000) return 3200.0; return 1350.0; } return 425.0; } Repare que, a cada teste, nós implementamos a modificação mais simples (TDD prega por simplicidade), que era justamente adicionar mais um if e colocar o valor fixo que fazia o teste passar. Não há modificação mais simples do que essa. O problema é que em algum momento fizemos tanto isso que nossa implementação passou a ficar muito longe da solução ideal. Veja que, dada a implementação atual, levá-la para a solução flexível que resolverá o problema de forma correta não será fácil. O código ficou complexo sem percebermos. Generalizando o ponto levantado: o desenvolvedor, em vez de partir para uma solução mais abstrata que resolveria o problema de forma genérica, prefere ficar postergando a implementação final, com a desculpa de estar praticando passos de bebê. Imagine isso em um problema do mundo real, complexo e cheio de casos excepcionais. Na prática, o que acontece é que o programador gasta tanto tempo "contornando" a solução que resolveria o problema de uma vez que, quando chega a hora de generalizar o problema, ele se perde. Em sessões de Coding Dojo, onde os programadores se juntam para praticar TDD, é comum ver sessões que duram muito tempo e, ao final, os desenvolvedores pouco avançaram no problema. Isso é simplesmente uma má interpretação do que são passos de bebê. O desenvolvedor deve buscar pela solução mais simples, e não pela modificação mais simples. Veja que a modificação mais simples não é necessariamente a solução mais simples. 4.3 PASSOS DE BEBÊ (OU BABY STEPS) 41 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com No começo, modificações mais simples podem ajudar o desenvolvedor a pensar melhor no problema que está resolvendo; esse foi o caso quando implementamos o return 1350.0 . Mas, caso o desenvolvedor continue somente implementando as modificações mais simples, isso pode afastá-lo da solução mais simples, que é o que o desenvolvedor deveria estar buscando ao final. Isso pode ser exemplificado pela sequência de if que fizemos na qual, em vez de partirmos para a solução correta (igual fizemos no capítulo anterior), optamos pela modificação mais simples. 42 4.3 PASSOS DE BEBÊ (OU BABY STEPS) E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com CODING DOJO Um coding dojo é um encontro de desenvolvedores que estão interessados em aprender alguma coisa nova, como uma linguagem de programação diferente, ou técnicas de desenvolvimento diferentes. O objetivo é criar um ambiente tranquilo e favorável a novos aprendizados. A prática mais comum é juntar alguns desenvolvedores e projetar uma máquina na parede. Dois desenvolvedores sentam em frente ao computador e começam a resolver algum problema predeterminado, usando a linguagem e/ou a prática que desejam aprender melhor. Geralmente esses dois desenvolvedores têm apenas alguns minutos para trabalhar. Enquanto eles trabalham, a plateia assiste e eventualmente sugere alterações para o "piloto e copiloto". Ao final do tempo (geralmente 7 minutos), o copiloto vira piloto, o piloto volta para a plateia, e alguém da plateia se torna "copiloto". Você pode ler mais sobre coding dojos, diferentes estilos, problemas sugeridos nas mais diversas referências na Internet (http://www.codingdojo.org). Veja as figuras a seguir. Nela temos o código e as possíveis soluções para o problema. Dentro desse conjunto, existem as que são resolvidas pelas mudanças mais simples, que podem, ou não, ser a solução mais simples também para o problema: 4.3 PASSOS DE BEBÊ (OU BABY STEPS) 43 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Se o desenvolvedor continuamente opta pela modificação mais simples que não é a solução mais simples, isso só vai afastá-lo da melhor solução: 44 4.3 PASSOS DE BEBÊ (OU BABY STEPS) E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com 4.3 PASSOS DE BEBÊ (OU BABY STEPS) 45 E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com Em nosso exemplo, veja que a solução mais simples acabou sendo o uso de if s. Sabemos que todo condicional faz o código ser mais difícil de ser mantido e de ser testado. Sabemos que essa não é a melhor solução para o problema; o uso de polimorfismo seria mais adequado. Nesse problema específico, discutiremos como resolvê-lo da melhor forma nos capítulos posteriores. Novamente, resumindo a discussão: a mudança mais simples para resolver um problema não é necessariamente a solução mais simples para resolvê-lo. Os passos de bebê servem para ajudar o desenvolvedor a entender melhor o problema e diminuir o passo caso ele não se sinta confortável com o problema que está resolvendo. Se o desenvolvedor está implementando um trecho de código complexo e tem dúvidas sobre como fazê-lo, ele pode desacelerar e fazer implementações mais simples. Mas caso esteja seguro sobre o problema, ele pode tomar passos maiores e ir direto para uma implementação mais abstrata. O próprio Kent Beck comenta sobre isso em seu livro. Em seu exemplo, ele implementa uma classe Dinheiro , responsável por representar uma unidade monetária e realizar operações sobre ela. No começo, ele toma passos simplórios, como os feitos anteriormente, mas finaliza o capítulo dizendo que ele não toma passos pequenos o tempo todo; mas que fica feliz por poder tomá-los quando necessário. 46 4.3 PASSOS DE BEBÊ (OU BABY STEPS) E-book gerado especialmente para Henrique Pimentel - henriq.pimentel@gmail.com CÓDIGO DUPLICADO ENTRE O CÓDIGO DE TESTE E O CÓDIGO DE PRODUÇÃO Todo desenvolvedor sabe que deve refatorar qualquer duplicidade de código no momento que a encontrar. No próprio problema da soma, a duplicidade já aparece logo no primeiro teste: repare que o "1", "2" existentes no código de testes está duplicado com o "I" e "II" no código de produção. É fácil ver que cada "I", adiciona 1 no número final. Sim, você deve ficar atento com duplicidade de código entre a classe de teste e a classe de produção. Nesse caso, após o teste ficar verde, o desenvolvedor poderia já refatorar o código
Compartilhar