Desenvolvimento Orientado a Testes
Test-driven development (TDD) é um caso especial de programação test-first que adiciona o elemento de design contínuo.
A programação test-first envolve a produção de testes de unidade automatizados para código de produção, antes você escreve esse código de produção. Em vez de escrever testes depois (ou, mais tipicamente, nunca escrever esses testes), você sempre começa com um teste de unidade. Para cada pequeno pedaço de funcionalidade no código de produção, você primeiro constrói e executa um pequeno (idealmente muito pequeno), teste focado que especifica e valida o que o código fará. Esse teste pode nem compilar, a princípio, porque nem todas as classes e métodos necessários podem existir. No entanto, funciona como uma espécie de especificação executável. Em seguida, você o compila com o mínimo de código de produção, para que possa executá-lo e vê-lo falhar. (Às vezes, você espera que ele falhe, e ele passa, o que é uma informação útil.) Em seguida, você produz exatamente o código que permitirá que o teste seja aprovado.
Essa técnica parece estranha, a princípio, para alguns programadores que a experimentam. É um pouco como alpinistas escalando uma parede de pedra, colocando âncoras na parede enquanto avançam. Por que se dar a todo esse trabalho? Certamente isso diminui sua velocidade consideravelmente? A resposta é que só faz sentido se você acabar confiando forte e repetidamente nesses testes de unidade posteriormente. Aqueles que praticam test-first regularmente afirmam que esses testes de unidade mais do que compensam o esforço necessário para escrevê-los.
Para o trabalho de teste inicial, você normalmente usará uma das famílias xUnit de estruturas de teste de unidade automatizadas (JUnit para Java, NUnit para C#, etc). Essas estruturas tornam bastante simples criar, executar, organizar e gerenciar grandes conjuntos de testes de unidade. (No mundo Java, pelo menos, eles estão cada vez mais bem integrados aos melhores IDEs.) Isso é bom, porque conforme você trabalha testando primeiro, você acumula muitos, muitos testes de unidade.
Benefícios do trabalho test-first
Conjuntos completos de testes de unidades automatizados servem como uma espécie de rede para detectar bugs. Eles identificam, de forma precisa e determinística, o comportamento atual do sistema. Boas equipes de teste descobrem que obtêm substancialmente menos defeitos ao longo do ciclo de vida do sistema e gastam muito menos tempo depurando. Testes de unidade bem escritos também servem como excelente documentação de design que está sempre, por definição, em sincronia com o código de produção. Um benefício um tanto inesperado: muitos programadores relatam que “a pequena barra verde” que mostra que todos os testes estão rodando limpos torna-se viciante. Uma vez que você está acostumado a esses pequenos e frequentes feedbacks positivos sobre a saúde do seu código, é realmente difícil desistir disso. Finalmente, se o comportamento do seu código for definido com muitos bons testes de unidade, é muito safer para você refatorar o código. Se uma refatoração (ou um ajuste de desempenho ou qualquer outra alteração) apresentar um bug, seus testes o alertarão rapidamente.
Desenvolvimento orientado a testes: indo além
Test-driven development (TDD) é um caso especial de programação test-first que adiciona o elemento de design contínuo. Com o TDD, o design do sistema não é limitado por um documento de design em papel. Em vez disso, você permite que o processo de escrever testes e código de produção conduza o design à medida que avança. A cada poucos minutos, você refatora para simplificar e esclarecer. Se você puder facilmente imaginar um método, classe ou modelo de objeto inteiro mais claro e limpo, você refatorará nessa direção, protegido o tempo todo por um conjunto sólido de testes de unidade. A presunção por trás do TDD é que você não pode realmente dizer qual design irá atendê-lo melhor até que você tenha os braços afundados no código. Conforme você aprende sobre o que realmente funciona e o que não funciona, você está na melhor posição possível para aplicar esses insights, enquanto eles ainda estão frescos em sua mente. E toda essa atividade é protegida por seus conjuntos de testes de unidade automatizados.
Você pode começar com uma boa quantidade de design inicial, embora seja mais comum começar com bastante design de código simples; alguns esboços UML de quadro branco geralmente são suficientes no mundo da programação extrema. Mas quanto design você começa importa menos, com TDD, do que quanto você permite que esse design diverja de seu ponto de partida conforme você avança. Você pode não fazer mudanças arquitetônicas abrangentes, mas pode refatorar o modelo de objeto em grande parte, se isso parecer a coisa mais sensata a fazer. Algumas lojas têm mais latitude política para implementar o verdadeiro TDD do que outras.
Testar primeiro x depurar
É útil comparar o esforço gasto escrevendo testes antecipadamente com o tempo gasto na depuração. A depuração geralmente envolve examinar grandes quantidades de código. O trabalho de teste permite que você se concentre em um pedaço pequeno, no qual menos coisas podem dar errado. É difícil para os gerentes prever quanto tempo a depuração realmente levará. E, em certo sentido, tanto esforço de depuração é desperdiçado. A depuração envolve investimento de tempo, scaffolding e infraestrutura (pontos de interrupção, observação de variáveis temporárias, instruções de impressão) que são essencialmente descartáveis. Depois de encontrar e corrigir o bug, toda essa análise é essencialmente perdida. E se não está totalmente perdido para você, certamente está perdido para outros programadores que mantêm ou estendem esse código. Com o trabalho test-first, os testes estão disponíveis para todos usarem, para sempre. Se um bug reaparecer de alguma forma, o mesmo teste que o detectou uma vez pode detectá-lo novamente. Se um bug aparecer porque não há teste correspondente, você pode escrever um teste que o capture a partir de então. Desta forma, muitos praticantes de teste afirmam que é o epítome de trabalhar de forma mais inteligente em vez de mais difícil.
Técnica e ferramentas de testar primeiro
Nem sempre é trivial escrever um teste unitário para cada aspecto do comportamento de um sistema. E quanto às GUIs? E quanto aos EJBs e outras criaturas cujas vidas são gerenciadas por estruturas baseadas em contêineres? E quanto aos bancos de dados e persistência em geral? Como você testa se uma exceção foi lançada corretamente? Como você testa os níveis de desempenho? Como você mede a cobertura, a granularidade e a qualidade do teste? Essas perguntas estão sendo respondidas pela comunidade test-first com um conjunto de ferramentas e técnicas em constante evolução. Uma tremenda engenhosidade continua sendo aplicada para tornar possível cobrir todos os aspectos do comportamento de um sistema com testes unitários. Por exemplo, muitas vezes faz sentido testar um componente de um sistema isoladamente de seus colaboradores e recursos externos, usando objetos falsos e simulados. Sem essas simulações ou falsificações, seus testes de unidade talvez não consigam instanciar o objeto em teste. Ou, no caso de recursos externos, como conexões de rede, bancos de dados ou GUIs, o uso da coisa real em um teste pode retardá-lo enormemente, enquanto o uso de uma versão falsa ou simulada mantém tudo funcionando rapidamente na memória. E embora alguns aspectos da funcionalidade possam sempre exigir teste manual, a percentagem para a qual isso é indiscutivelmente verdade continua a diminuir.