A importância de uma abordagem específica para o teste depende de quão crítica é a missão do sistema em desenvolvimento e de quanto outro sistema crítico depende dela. Um script simples de livro de visitas para o seu site dificilmente pode ser considerado de missão crítica, mas se o site em que ele é executado pode potencialmente ser comprometido por um bug que permitia entrada não filtrada no banco de dados e esse site oferece algum serviço vital, então de repente se torna muito mais É importante que o script do livro de visitas seja completamente testado. O mesmo vale também para o código da estrutura / biblioteca. Se você desenvolver uma estrutura com um bug, todos os aplicativos que usam esse recurso da estrutura também terão esse mesmo bug.
O desenvolvimento orientado a testes oferece uma camada extra de segurança quando se trata de testes. Se você escrever os testes ao lado ou mesmo depois do código que deseja testar, haverá um risco real de que os testes sejam errados. Se você escrever todos os testes primeiro, então como o código funciona internamente não poderá influenciar para o que você escreve os testes e, portanto, há menos possibilidade de você escrever inadvertidamente testes que pensam que uma saída incorreta específica está correta.
O desenvolvimento orientado a testes também incentiva seus desenvolvedores a escrever um código fácil de testar, porque eles não querem se esforçar ainda mais! O código fácil de testar tende a ser fácil de entender, reutilizar e manter.
E a manutenção é onde você realmente colherá as recompensas do TDD. A grande maioria do esforço de programação gasto em software está relacionada à manutenção. Isso significa fazer alterações no código ativo para fornecer novos recursos, corrigir bugs ou adaptá-lo a novas situações. Ao fazer essas alterações, você quer ter certeza de que as alterações efetuadas têm o efeito desejado e, mais importante, não causam efeitos inesperados. Se você possui um conjunto de testes completo para o seu código, é fácil verificar se as alterações feitas não estão quebrando outra coisa; se as alterações feitas quebram alguma outra coisa, você pode localizar rapidamente o motivo. Os benefícios são de longo prazo.
Você disse o seguinte em sua pergunta:
Vejo algum benefício em escrever testes para algumas coisas, mas muito poucas. E embora eu goste da ideia de escrever o teste primeiro, acho que passo substancialmente mais tempo tentando depurar meus testes para que eles digam o que eu realmente quero dizer do que depurando o código real. Isso provavelmente ocorre porque o código de teste geralmente é substancialmente mais complicado do que o código que ele testa. Espero que isso seja apenas inexperiência com as ferramentas disponíveis (rspec neste caso).
Isso parece me sugerir que você não está fazendo testes. Um teste de unidade deve ser extremamente simples, apenas uma sequência de chamadas de método, seguida de uma asserção para comparar o resultado esperado com o resultado real. Eles são feitos para serem simples, porque os erros em seus testes seriam desastrosos e, se você introduzir loops, ramificações ou outro programa, lançará controle no teste, será mais provável que o teste tenha um bug introduzido nele. Se você está gastando muito tempo depurando testes, isso indica que seus testes são muito complicados e você deve simplificá-los.
Se os testes não puderem ser simplificados, apenas esse fato sugere que há algo errado com o código em teste. Por exemplo, se sua classe possui métodos longos, métodos com muitas instruções if / elseif / else ou switch ou um número alto de métodos que possuem interações complexas ditadas pelo estado atual da classe, os testes precisam, por necessidade, ser extremamente complexos para fornecer cobertura completa do código e testar todas as eventualidades. Se sua classe tiver dependências codificadas em outras classes, isso aumentará novamente o número de hoops para os quais você terá que pular para testar efetivamente seu código.
Se você mantiver suas classes pequenas e altamente focadas, com métodos curtos com poucos caminhos de execução e tentar eliminar o estado interno, os testes poderão ser simplificados. E esse é o cerne da questão. Um bom código é inerentemente fácil de testar. Se o código não for fácil de testar, provavelmente há algo errado com ele.
Escrever testes de unidade é algo que beneficia você a longo prazo, e evitá-los é simplesmente armazenar problemas para mais tarde. Você pode não estar familiarizado com o conceito de dívida técnica, mas funciona muito como dívida financeira. Não escrever testes, não comentar códigos, escrever em dependências codificadas e assim por diante são formas de endividar-se. Você "empresta" tempo cortando os cantos desde o início e isso pode ajudá-lo a atingir um prazo apertado, mas o tempo que você economiza no início do projeto é emprestado. Cada dia que passa sem limpar o código, comentá-lo adequadamente ou criar um conjunto de testes custará seu interesse. Quanto mais tempo, mais juros se acumulam. Eventualmente, você descobrirá que seu código se tornou uma bagunça emaranhada na qual você não pode fazer alterações sem desencadear consequências não intencionais.
Você pode pensar em escrever testes de unidade com antecedência e mantê-los atualizados como uma forma de "crédito técnico". Você está investindo tempo no banco gastando-o no início do projeto seguindo as boas práticas. Você ganhará interesse nessa previsão mais tarde, quando chegar à fase de manutenção do projeto. Quando você deseja fazer uma alteração, pode facilmente validar a correção da alteração e se ela não tem efeitos colaterais indesejados, além de obter atualizações rapidamente e sem problemas. Se os erros aparecerem, você poderá adicionar um novo teste de unidade que exerça o erro e, em seguida, corrigir o erro no código. Na próxima execução do teste de unidade, você poderá verificar se o bug foi corrigido e se não causou outros problemas. Além disso, você evitará "regressões",
TL: DR - Sim, eles são uma ajuda do mundo real, mas são um investimento. Os benefícios só se tornam aparentes mais tarde.