Test-driven development

Test Driven Development (TDD) ou em português Desenvolvimento guiado por testes é uma técnica de desenvolvimento de software que se relaciona com o conceito de verificação e validação e se baseia em um ciclo curto de repetições: Primeiramente o desenvolvedor escreve um caso de teste automatizado que define uma melhoria desejada ou uma nova funcionalidade. Então, é produzido código que possa ser validado pelo teste para posteriormente o código ser refatorado para um código sob padrões aceitáveis. Kent Beck, considerado o criador ou o 'descobridor' da técnica, declarou em 2003 que TDD encoraja designs de código simples e inspira confiança.[1] Desenvolvimento dirigido por testes é relacionado a conceitos de programação de Extreme Programming, iniciado em 1999,[2] mas recentemente tem-se criado maior interesse pela mesma em função de seus próprios ideais.[3] Através de TDD, programadores podem aplicar o conceito de melhorar e depurar código legado desenvolvido a partir de técnicas antigas.[4]


TDD aplicado ao desenvolvimento de um algoritmo em JavaScript que transforma números romanos em números ordinários

Requisitos editar

Desenvolvimento dirigido por testes[5] requer dos desenvolvedores criar testes automatizados que definam requisitos em código antes de escrever o código da aplicação. Os testes contém asserções que podem ser verdadeiras ou falsas. Após as mesmas serem consideradas verdadeiras após sua execução, os testes confirmam o comportamento correto, permitindo os desenvolvedores evoluir e refatorar o código. Normalmente todos os testes são efetuados de forma continua de acordo com o desenvolvimento cada funcionalidade criada deve ser acompanhada de um teste bem descrito e projetado, então deve-se escolher a área do projeto ou requisitos da tarefa para melhor orientar o desenvolvimento destes testes.

Desenvolvedores normalmente usam frameworks de testes, como xUnit, para criar e executar automaticamente uma série de casos de teste.

Evolução histórica editar

Kent Beck, considerado o criador ou o descobridor da técnica, declarou em 2003 que TDD encoraja designs de código simples e inspira confiança. Desenvolvimento dirigido por testes é relacionado a conceitos de programação de Extreme Programming, iniciado em O desenvolvimento guiado por testes, em língua inglesa Test Driven Development (TDD), teve origem na metodologia ágil criado por Kent Beck. Ela se tornou mais conhecida entre desenvolvedores após a publicação do livro TDD by exemplo de Kent Beck em 2002. Ainda que utilização já ocorresse há algumas décadas, segundo Larman e Basili (2003), o manifesto ágil contribuiu muito para impulsioná-la. próprios ideais. Através de TDD, programadores podem aplicar o conceito de melhorar e depurar código legado desenvolvido a partir de técnicas antigas.

Ciclo de desenvolvimento editar

1. Adicione um teste editar

Para escrever um teste, o desenvolvedor precisa claramente entender as especificações e requisitos da funcionalidade. O desenvolvedor pode fazer isso através de caso de uso que cubram os requisitos e exceções condicionais.

Esta é a diferenciação entre desenvolvimento dirigido a testes entre escrever testes de unidade depois do código desenvolvido. Ele torna o desenvolvedor focado nos requisitos antes do código, que é uma sutil porem importante diferença.

Em um estudo por George e Willians (2003), com programadores constatou-se que: (87,5%) acreditavam que TDD facilita uma melhor abordagem e compreensão dos requisitos; 95,8% acreditavam que o tempo de depuração foi reduzido; 78% acreditavam que a produtividade do time aumentou; 92% acreditavam que melhorou a qualidade do código; 79% acreditavam que o design ficou mais simples; e no total 71% acreditavam que a abordagem foi eficaz.

2. Execute todos os testes e veja se algum deles falha editar

Esse passo valida se todos os testes estão funcionando corretamente e se o novo teste não traz nenhum equívoco, sem requerer nenhum código novo. Pode-se considerar que este passo então testa o próprio teste: ele regula a possibilidade de novo teste passar.

O novo teste deve então falhar pela razão esperada: a funcionalidade não foi desenvolvida. Isto aumenta a confiança (por outro lado não exatamente a garante) que se está testando a coisa certa, e que o teste somente irá passar nos casos intencionados.

3. Escrever código editar

O próximo passo é escrever código que irá ocasionar ao teste passar. O novo código escrito até esse ponto poderá não ser perfeito e pode, por exemplo, passar no teste de uma forma não elegante. Isso é aceitável porque posteriormente ele será melhorado.

O importante é que o código escrito deve ser construído somente para passar no teste; nenhuma funcionalidade (muito menos não testada) deve ser predita ou permitida em qualquer ponto.

4. Execute os testes automatizados e veja-os executarem com sucesso editar

Se todos os testes passam agora, o programador pode ficar confiante de que o código possui todos os requisitos testados. Este é um bom ponto que inicia o passo final do ciclo TDD.

5. Refatorar código editar

Nesse ponto o código pode ser limpo como necessário. Ao reexecutar os testes, o desenvolvedor pode confiar que a refatoração não é um processo danoso a qualquer funcionalidade existente. Um conceito relevante nesse momento é o de remoção de duplicação de código, considerado um importante aspecto ao design de um software. Nesse caso, entretanto, isso aplica remover qualquer duplicação entre código de teste e código de produção — por exemplo magic numbers or strings que nós repetimos nos testes e no código de produção, de forma que faça o teste passar no passo 3.

6. Repita tudo editar

Iniciando com outro teste, o ciclo é então repetido, empurrando a funcionalidade a frente. O tamanho dos passos deve ser pequeno - tão quanto de 1 a 10 edições de texto entre cada execução de testes. Se novo código não satisfaz rapidamente um novo teste, ou outros testes falham inesperadamente, o programador deve desfazer ou reverter as alterações ao invés do uso de excessiva depuração. A integração contínua ajuda a prover pontos reversíveis. É importante lembrar que ao usar bibliotecas externas não é interessante gerar incrementos tão pequenos que possam efetivamente testar a biblioteca ,[3] a menos que haja alguma razão para acreditar que a biblioteca tenha defeitos ou não seja suficientemente completada com suas funcionalidades de forma a servir às necessidades do programa em que está sendo escrito.

Estilos de desenvolvimento editar

Há vários aspectos ao usar desenvolvimento dirigido a testes, por exemplo os princípios "Keep It Simple, Stupid" (KISS) e "You Ain`t Gonna Need It" (YAGNI). Focando em escrever código somente para os testes passarem, o design do sistema pode ser mais limpo e claro do que o que é alcançado por outros métodos.[1] Em Test-Driven Developmente By Example Kent Beck sugere o princípio "Fake it, till you make it". Para alcançar altos níveis conceituais de design (como o uso de Design Pattern), testes são escritos de forma que possam gerar o design. O código pode acabar permanecendo mais simples do que o padrão alvo, entretanto ele deve passar em todos os testes requeridos. Isto pode ser inaceitável de primeira mas ele permite o desenvolvedor focar somente no que é importante.

Falhar primeiro os casos de testes. A ideia é garantir que o teste realmente funciona e consegue capturar um erro. Assim que ele é mostrado, a funcionalidade almejada pode ser implementada. Isto tem sido apontado como o "Test-Driven Mantra", conhecido como vermelho/verde/refatorar onde vermelho significa falhar onde pelo menos uma asserção falha ,e verde é passar, que significa que todas as asserções foram verdadeiras.

Desenvolvimento dirigido a testes constantemente repete os passos de adicionar casos de teste que falham, passando e refatorando-os. Ao receber o resultado esperado em cada estágio reforça ao desenvolvedor o modelo mental do código, aumentando a confiança e incrementando a produtividade.

Práticas avançadas de desenvolvimento dirigido a testes encaminham para o desenvolvimento dirigido a testes de aceitação (ATDD), onde o critério especificado pelo cliente é automatizado em testes de aceitação, que então levam ao tradicional processo de desenvolvimento dirigido a testes de unidade. Este processo garante que o cliente tem um mecanismo automatizado para decidir como o software atende suas necessidades. Com ATDD, o desenvolvimento em equipe tem objetivo específico para satisfazer, e os testes de aceitação os mantém continuamente focados em o que o cliente realmente deseja daquela funcionalidade.

Benefícios editar

Um estudo de 2005 descobriu que usar TDD significava escrever mais testes, e logo, programadores que escreviam mais testes tendiam a ser mais produtivos.[6] Hipóteses relacionando a qualidade de código e uma correlação direta entre TDD e produtividade foram inconclusivas.[7]

Desenvolvedores usando TDD puramente em novos projetos reportaram que raramente necessitaram a utilização de um depurador. Usado em junção com um Sistema de controle de versão, quando testes falham inesperadamente, reverter o código para a última versão em que os testes passaram pode ser mais produtivo do que depurar.[8][9]

Desenvolvimento dirigido por testes oferece mais do que somente um maneira simples de validação e de correção, pode orientar o design de um programa. Por focar em casos de testes primeiramente, deve-se imaginar como a funcionalidade será usada pelo cliente. Logo, o programador é somente com a interface e não com a implementação. Este benefício é complementar ao Design por Contrato, que através torna os casos de testes muito mais do que asserções matemáticas ou pré-concepções.

O poder que TDD oferece é a habilidade de pegar pequenas partes quando requeridas. Isso permite o desenvolvedor focar como objetivo fazer os testes atuais passarem. Casos excepcionais e tratamento de erros não são considerados inicialmente. Testes que criam estas circunstâncias estranhas são implementadas separadamente. Outra vantagem é que TDD quando usado apropriadamente, garante que todo o código desenvolvido seja coberto por um teste. Isto fornece a equipe de desenvolvimento, e ao usuários, subsequentemente, um grande nível de confiança ao código.

Enquanto que é verdade que mais código é necessário ao usar TDD do que sem TDD, devido aos códigos de teste, o tempo total de implementação é tipicamente menor.[10] Um grande número de testes ajudam a limitar o número de defeitos no código. A natureza periódica ajuda a capturar defeitos inicialmente no ciclo de desenvolvimento, prevenindo-os de se tornarem grandes e endêmicos problemas. Eliminando defeitos cedo no processo normalmente evita longos e tediantes períodos de depuração posteriores em um projeto.

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 reescritas, desenvolvidas e testadas independentemente e integradas depois. Isto implica menores e mais classes, evitando o acoplamento e permitindo interfaces mais limpas. O uso de Mock Object é um contribuidor para a modularização do código, pois este recurso requer que o código seja escrito de forma que possa ser facilmente trocado entre versões Mock, usados em testes de unidade, e "reais", usados na aplicação.

Devido a fato de que nenhum código é escrito a não ser para passar em um teste que esteja falhando, testes automatizados tendem a cobrir cada caminho de código. Por exemplo, para que um desenvolvedor possa adicionar um caminho alternativo "senão" em um "se" , o desenvolvedor poderia primeiramente escrever um teste que motive o fluxo alternativo. Como resultado, os testes automatizados TDD tendem a ser mais perfeitos: eles irão mostrar qualquer mudança inesperada no comportamento do código. Isto ajuda a identificar problemas cedo que poderiam aparecer ao consertar uma funcionalidade que ao modificada, inesperadamente altera outra funcionalidade.

Limitações editar

  1. Desenvolvimento dirigido com testes é difícil de usar em situações onde testes totalmente funcionais são requeridos para determinar o sucesso ou falha. Exemplos disso são interfaces de usuários, programas que trabalham com base de dados, e muitos outros que dependem de configurações específicas de rede. TDD encoraja os desenvolvedores a incluir o mínimo de código funcional em módulos e maximizar a lógica, que é extraída em código de teste, usando Fakes mocks para representar o mundo externo.
  2. Suporte gerencial é essencial. Se toda a organização não acreditar que TDD é para melhorar o produto, o gerenciamento irá considerar que o tempo gasto escrevendo teste é desperdício.[11]
  3. Os próprios testes se tornam parte da manutenção do projeto. Testes mal escritos, por exemplo, que incluem strings de erro embutidas ou aqueles que são suceptíveis a falha, são caros de manter. Há um risco em testes que geram falsas falhas de tenderem a serem ignorados. Assim quando uma falha real ocorre, ela pode não ser detectada. É possível escrever testes de baixa e fácil manutenção, por exemplo pelo reuso das strings de erro, podendo ser o objetivo durante a fase de refatoração descrita acima.
  4. O nível de cobertura e detalhamento de teste alcançado durante repetitivos ciclos de TDD não pode ser facilmente recriado em uma data tardia. Com o passar do tempo os testes vão se tornando gradativamente preciosos. Se uma arquitetura pobre, um mal design ou uma estratégia de teste mal feita acarretar em mudança tardia, fazendo com que dezenas de testes falhem, por outro lado eles são individualmente consertáveis. Entretanto, simplesmente deletando, desabilitando ou alterando-os vagamente poderá criar buracos indetectáveis na cobertura de testes.
  5. Lacunas inesperadas na cobertura de teste podem existir ou ocorrer por uma série de razões. Talvez um ou mais desenvolvedores em uma equipe não foram submetidos ao uso de TDD e não escrevem testes apropriadamente, talvez muitos conjuntos de testes foram invalidados, excluídos ou desabilitados acidentalmente ou com o intuito de melhorá-los posteriormente. Se isso acontece, a certeza é de que um enorme conjunto de testes TDD serão corrigidos tardiamente e refatorações serão mal acopladas. Alterações podem ser feitas não resultando em falhas, entretanto, na verdade, bugs estão sendo introduzidos imperceptivelmente, permanecendo indetectáveis.
  6. Testes de unidade criados em um ambiente de desenvolvimento dirigido por testes são tipicamente criados pelo desenvolvedor que irá então escrever o código que está sendo testado. Os testes podem consequentemente compartilhar os 'pontos cegos' no código: Se por exemplo, um desenvolvedor não realizar determinadas entradas de parâmetros que precisam ser checadas, muito provavelmente nem o teste nem o código irá verificar essas entradas. Logo, se o desenvolvedor interpreta mal a especificação dos requisitos para o módulo que está sendo desenvolvido, tanto os testes como o código estarão errados.
  7. O alto número de testes de unidades pode trazer um falso senso de segurança, resultando em menor nível de atividades de garantia de qualidade, como testes de integração e aceitação.

Visibilidade de código editar

O conjunto de teste claramente possibilita acessar o código que está se testando. Por outro lado critérios normais de design como Information hiding, encapsulamento e separação de conceitos não devem ser comprometidos. Consequentemente teste de unidade são normalmente escritos no mesmo projeto ou módulo que o código está sendo testado.

Ao usar design orientado a objetos, este não provê acesso ao métodos e dados privados. Consequentemente, trabalho extra pode ser necessário para testes de unidade. Em Java e outras liguagens, um desenvolvedor pode usar reflexão para acessar campos que são marcados como privados.[12] Alternativamente, uma classe inerente pode ser usada para suportar os testes de unidade, assim tendo visibilidade dos membros e atributos da classe. No .NET e em muitas outras linguagens, classes parciais podem ser usadas para expor métodos privados e dados para os testes acessarem.

O importante é que modificações brutas no testes não apareçam no código de produção. Em C# e em outras linguagens, diretivas de de pré-compilação como #if DEBUG ... #endif pode ser posicionadas em volta de classes adicionais e realmente prevenir todos os outros códigos relacionados a teste de serem compilados no código de lançamento. Isto então significa que o código lançado não é exatamente o mesmo de quando testado. A execução regular de poucos mais completos, testes de integração de aceitação ao final do lançamento do código podem garantir que não existe código de produção que subitamente dependa dos testes. Há muito debate entre os praticantes de TDD, documentados nos seus blogs e outros locais, com a dúvida se é de bom julgamento testar métodos privados e protegidos e dados de qualquer maneira. Muitos argumentam que poderia ser suficiente testar qualquer classe através da interface pública, considerando membros privados como meramente detalhes de implementações que podem mudar, e se deveria ser permitido fazer sem quebrar nenhum teste. Outros dizem que aspectos cruciais de uma funcionalidade podem ser implementados em métodos privados, e que ao fazer desta forma eles são indiretamente testados através da interface pública obscurecendo a ocorrência dos mesmos: Testes de unidade são para testar a menor unidade de funcionalidade possível.[13][14]

Fakes, mocks e testes de integração editar

Testes de unidades são assim chamados por cada teste exercitar uma unidade de código. Se um módulo tem centenas de testes de unidade ou somente cinco é irrelevante. Um conjunto de testes em TDD nunca cruza os limites de um programa, nem deixa conexões de rede perdidas. Ao fazer essas ações, o mesmo introduz intervalos de tempo que podem tornar testes lentos ao serem executados, desencorajando desenvolvedores de executar toda a suite de testes. Introduzir dependências de módulos externos ou data transforma teste de unidade em testes de integração. Se um módulo se comporta mal em uma cadeia de módulos interrelacionados, não fica imediatamente claro onde olhar a causa da falha.

Quando o código em desenvolvimento depende confiavelmente de uma base de dados, um serviço web ou qualquer outro processo externo ou serviço, obrigando em um separação unitária de teste é então uma oportunidade forçada de criar um design de código mais modular, mais testável e reusável.[15] Dois passos são necessários:

  1. Toda vez que um acesso externo é necessário no design final, uma interface deve ser definida de forma a descrever que acessos irão ser disponíveis. O princípio de inversão de dependência fornece benefícios nessa situação em TDD.
  2. A interface deve ser implementada de duas maneiras, uma que realmente acessa o processo externo, e outra que é um fake ou mock. Objetos fake precisam fazer um pouco mais do que adicionar mensagens "Objeto Pessoa salvo" criando registro de rastreio, identificando que asserções podem executadas para verificar o comportamento correto. Objetos mock diferem pelo fato que eles mesmos contém asserções que podem fazer com que o teste falhe. Exemplo:
Se o nome da pessoa e outro dado não é como esperado. métodos de objetos fake e mock que retornem dados, aparentemente de uma armazenamento de dados ou de usuário,pode ajudar ao processo de teste sempre retornar o mesmo, dados que testes pode confiar. Eles podem ser muito usáveis em predefinidos modos de falha em que rotinas de tratamento de erro pode ser desenvolvidas e confiavelmente testadas. Serviços fake dentre outros armazenamento de dados podem ser usáveis em TDD: um serviço fake de criptografia pode não, criptografar o dado passado;[necessário esclarecer] serviços fake de número aleatório podem sempre retornar 1. Implementações de fakes e mocks são exemplos de injeção de dependência.

A proposta da injeção de dependência é que a base de dados ou qualquer ou código de acesso externo nunca é testado pelo processo TDD. Para desviar de erros que possam aparecer em função disso, outros testes com a real implementação das interfaces discutidas acima são necessários. Esses testes são separados dos testes unitários e são realmente testes de integração. Eles serão poucos, e eles precisam ser menos executados os testes de unidades. Eles podem ser implementados sem nenhum problema usando o mesmo framework de testes, como xUnit .

Testes de integração que alteram qualquer armazenamento persistente ou banco de dados deve ser designado cuidadosamente, levando em consideração os estados iniciais e finais dos arquivos e base de dados, independente se um teste falha. Isto é muitas vezes adquirido usando várias combinações das seguintes técnicas:

  • O método TearDown, que é integrado ao muitos frameworks de teste.
  • Estruturas try...catch...finally de tratamento de exceções, quando disponíveis.
  • Transações de banco de dados onde ela atomicamente inclui talvez um escrita, uma leitura e uma operação de deleção.
  • Capturando o "estado" do banco de dados antes de executar qualquer teste e após a execução, desfasar para o "estado" antes do teste ser executado. Isso pode ser automatizado usando a framework como o Ant ou NAnt ou um sistema de integração contínua como o CruiseControl.
  • Inicializando o banco de dados para um estado limpo antes dos testes, ao invés de limpar depois da execução dos testes. Isto pode ser relevante onde limpar depois pode tornar difícil de diagnosticar falhas de teste ao deletar o estado final do banco de dados antes que um diagnótico detalhado possa ser feito.

Framework como Moq, jMock, NMock, EasyMock, TypeMock, jMockit, Mockito, PowerMock e Rhino Mocks existem para tornar o processo de criar e usar objetos mock complexos facilmente.

Ver também editar

Referências editar

  1. a b Beck, K. Test-Driven Development by Example, Addison Wesley - Vaseem, 2003
  2. "Extreme Programming", Computerworld (online), December 2001, webpage: Computerworld-appdev-92.
  3. a b Newkirk, JW and Vorontsov, AA. Test-Driven Development in Microsoft .NET, Microsoft Press, 2004.
  4. Feathers, M. Working Effectively with Legacy Code, Prentice Hall, 2004
  5. «What is TDD? Everything About Test Driven Development». Insights on Latest Technologies - Simform Blog (em inglês). 15 de abril de 2019. Consultado em 27 de agosto de 2021 
  6. Erdogmus, Hakan; Morisio, Torchiano. «On the Effectiveness of Test-first Approach to Programming». Proceedings of the IEEE Transactions on Software Engineering, 31(1). January 2005. (NRC 47445). Consultado em 14 de janeiro de 2008. Arquivado do original em 27 de agosto de 2011. We found that test-first students on average wrote more tests and, in turn, students who wrote more tests tended to be more productive. 
  7. Proffitt, Jacob. «TDD Proven Effective! Or is it?». Consultado em 21 de fevereiro de 2008. Arquivado do original em 27 de agosto de 2011. So TDD's relationship to quality is problematic at best. Its relationship to productivity is more interesting. I hope there's a follow-up study because the productivity numbers simply don't add up very well to me. There is an undeniable correlation between productivity and the number of tests, but that correlation is actually stronger in the non-TDD group (which had a single outlier compared to roughly half of the TDD group being outside the 95% band). 
  8. Clark, Mike. «Test-Driven Development with JUnit Workshop». Clarkware Consulting, Inc. Consultado em 1 de novembro de 2007. In fact, test-driven development actually helps you meet your deadlines by eliminating debugging time, minimizing design speculation and re-work, and reducing the cost and fear of changing working code. 
  9. Llopis, Noel (20 de fevereiro de 2005). «Stepping Through the Looking Glass: Test-Driven Game Development (Part 1)». Games from Within. Consultado em 1 de novembro de 2007. Arquivado do original em 13 de outubro de 2007. Comparing [TDD] to the non-test-driven development approach, you're replacing all the mental checking and debugger stepping with code that verifies that your program does exactly what you intended it to do. 
  10. Müller, Matthias M.; Padberg, Frank. «About the Return on Investment of Test-Driven Development» (PDF). Universität Karlsruhe, Germany. 6 páginas. Consultado em 1 de novembro de 2007 
  11. Loughran, Steve (6 de novembro de 2006). «Testing» (PDF). HP Laboratories. Consultado em 12 de agosto de 2009 
  12. Burton, Ross (11 de dezembro de 2003). «Subverting Java Access Protection for Unit Testing». O'Reilly Media, Inc. Consultado em 12 de agosto de 2009 
  13. Newkirk, James (7 de junho de 2004). «Testing Private Methods/Member Variables - Should you or shouldn't you». Microsoft Corporation. Consultado em 12 de agosto de 2009 
  14. Stall, Tim (1 de março de 2005). «How to Test Private and Protected methods in .NET». CodeProject. Consultado em 12 de agosto de 2009 
  15. Fowler, Martin (1999). Refactoring - Improving the design of existing code. Boston: Addison Wesley Longman, Inc. ISBN 0-201-48567-2 

Notas editar

Ligações externas editar