Como as pessoas que fazem TDD lidam com a perda de trabalho ao realizar grandes refatorações

37

Por um tempo eu tenho tentado aprender a escrever testes de unidade para o meu código.

Inicialmente, comecei a fazer TDD verdadeiro, onde não escreveria nenhum código até escrever um teste com falha primeiro.

No entanto, recentemente tive um problema espinhoso para resolver, que envolvia muito código. Depois de passar algumas semanas escrevendo testes e depois codificando, cheguei à infeliz conclusão de que toda a minha abordagem não funcionaria e teria que jogar duas semanas de trabalho e começar de novo.

Essa é uma decisão bastante ruim quando você acabou de escrever o código, mas quando você também escreveu centenas de testes de unidade, fica ainda mais emocionalmente difícil simplesmente jogar tudo fora.

Não consigo deixar de pensar que perdi 3 ou 4 dias de esforço escrevendo esses testes, quando eu poderia apenas juntar o código para a prova de conceito e depois escrever os testes depois que fiquei satisfeito com a minha abordagem.

Como as pessoas que praticam TDD lidam adequadamente com essas situações? Em alguns casos, existe um argumento para adulterar as regras ou você sempre escreve os testes de maneira servil primeiro, mesmo quando esse código pode ser inútil?

GazTheDestroyer
fonte
6
A perfeição é alcançada, não quando não há mais nada a acrescentar, mas quando não há mais nada a ser levado. - Antoine de Saint-Exupery
mouviciel
12
Como é possível que todos os seus testes estejam errados? Por favor, explique como uma alteração na implementação invalida todos os testes que você escreveu.
9788 S.Lott
6
@ S.Lott: Os testes não estavam errados, eles não eram mais relevantes. Digamos que você esteja resolvendo parte de um problema usando números primos, então escreva uma classe para gerar números primos e escreva testes para essa classe para garantir que esteja funcionando. Agora você encontra outra solução totalmente diferente para o seu problema que não envolve números primos de forma alguma. Essa classe e seus testes agora são redundantes. Esta era a minha situação apenas com dez aulas e não apenas uma.
9262 GazTheDestroyer
5
@GazTheDestroyer parece-me que distinguir entre o código de teste e o código funcional é um erro - tudo faz parte do mesmo processo de desenvolvimento. É justo observar que o TDD possui uma sobrecarga que geralmente é recuperada mais adiante no processo de desenvolvimento e que parece que essa sobrecarga não ganhou nada nesse caso. Mas, igualmente, quanto os testes informaram sua compreensão das falhas da arquitetura? Também é importante notar que você está autorizado (ou melhor, encorajados ) para podar seus testes ao longo do tempo ... embora este é provavelmente um pouco extremo (-:
Murph
10
Vou ser semanticamente pedante e concordar com @ S.Lott aqui; o que você fez não é refatorar se resultar em jogar fora muitas classes e os testes para elas. Isso é re-arquitetar . A refatoração, especialmente no sentido de TDD, significa que os testes eram verdes, você alterou algum código interno, reexecutou os testes e eles permaneceram verdes.
Eric King

Respostas:

33

Sinto que há duas questões aqui. A primeira é que você não percebeu antecipadamente que seu design original pode não ser a melhor abordagem. Se você soubesse disso com antecedência, pode ter escolhido desenvolver um ou dois protótipos de descarte rápido , para explorar as possíveis opções de design e avaliar qual é a maneira mais promissora de seguir. Na prototipagem, você não precisa escrever um código de qualidade de produção e não precisa fazer testes de unidade em todos os cantos (ou em todos), pois seu único foco é aprender, não polir o código.

Agora, perceber que você precisa de prototipagem e experimentos, em vez de iniciar o desenvolvimento do código de produção imediatamente, nem sempre é fácil e nem sempre é possível. Armado com o conhecimento acabado de adquirir, você poderá reconhecer a necessidade de prototipagem da próxima vez. Ou não pode. Mas pelo menos você sabe agora que essa opção deve ser considerada. E isso por si só é um conhecimento importante.

A outra questão é IMHO com a sua percepção. Todos cometemos erros e é tão fácil ver em retrospecto o que deveríamos ter feito de maneira diferente. É assim que aprendemos. Anote seu investimento em testes de unidade como o preço de aprender que a prototipagem pode ser importante e supere-a. Apenas se esforce para não cometer o mesmo erro duas vezes :-)

Péter Török
fonte
2
Eu sabia que seria um problema difícil de resolver e que meu código seria um pouco exploratório, mas fiquei entusiasmado com meus recentes sucessos no TDD, então continuei escrevendo testes como havia feito, já que é isso que tudo a literatura TDD enfatiza muito. Então, sim, agora eu sei que as regras podem ser quebradas (que é realmente a minha pergunta) provavelmente vou considerar isso como experiência.
GazTheDestroyer
3
"Eu continuei escrevendo testes como havia feito, já que é isso que toda a literatura do TDD enfatiza tanto". Você provavelmente deve atualizar a pergunta com a fonte da sua ideia de que todos os testes devem ser escritos antes de qualquer código.
315 S.Lott
11
Não tenho essa ideia e não sei como você conseguiu isso com o comentário.
9788 GazeboDestroyer
11
Eu ia escrever uma resposta, mas virei a sua em seu lugar. Sim, um milhão de vezes sim: se você ainda não sabe como é sua arquitetura, escreva um protótipo descartável primeiro e não se preocupe em escrever testes de unidade durante a criação de protótipos.
Robert Harvey
11
@ WarrenP, certamente existem pessoas que pensam que o TDD é o único caminho verdadeiro (qualquer coisa pode ser transformada em religião, se você se esforçar o suficiente ;-). Eu prefiro ser pragmático. Para mim, o TDD é uma ferramenta na minha caixa de ferramentas, e eu o uso apenas quando ajuda, ao invés de dificultar, a resolver problemas.
Péter Török
8

O objetivo do TDD é que ele o força a escrever pequenos incrementos de código em pequenas funções , precisamente para evitar esse problema. Se você passou semanas escrevendo código em um domínio e todos os métodos utilitários que você escreveu se tornam inúteis quando você repensa a arquitetura, então seus métodos são quase certamente muito grandes em primeiro lugar. (Sim, eu sei que isso não é exatamente reconfortante agora ...)

Kilian Foth
fonte
3
Meus métodos não eram grandes, simplesmente se tornaram irrelevantes, dada a nova arquitetura, que não tinha nenhuma semelhança com a arquitetura antiga. Em parte porque a nova arquitetura era muito mais simples.
GazTheDestroyer
Tudo bem, se realmente não há nada reutilizável, você só pode reduzir suas perdas e seguir em frente. Mas a promessa do TDD é que ele alcance os mesmos objetivos mais rapidamente, mesmo que você escreva código de teste além do código do aplicativo. Se isso é verdade, e acredito firmemente que é, então pelo menos você chegou ao ponto em que percebeu como fazer a arquitetura em "algumas semanas" em vez de duas vezes mais.
Kilian Foth
11
@Kilian, re "a promessa do TDD é que ele alcance os mesmos objetivos mais rapidamente" - a quais objetivos você está se referindo aqui? É bastante óbvio que escrever testes de unidade junto com o próprio código de produção o torna mais lento inicialmente , em comparação com apenas produzir código. Eu diria que o TDD só pagará em longo prazo, devido à melhoria da qualidade e aos custos de manutenção reduzidos.
Péter Török
@ PéterTörök - Existem pessoas que insistem em que o TDD nunca tem nenhum custo, porque ele se paga no momento em que você escreveu o código. Esse certamente não é o meu caso, mas Killian parece acreditar por si mesmo.
Psd:
Bem ... se você não acredita nisso, de fato, se você não acredita que o TDD tenha uma recompensa substancial em vez de um custo, então não faz sentido fazê-lo, existe? Não apenas na situação muito específica que Gaz descreveu, mas de todo . Eu tenho medo que eu já dirigi esta discussão completamente fora de tópico :(
Kilian Foth
6

Brooks disse que "planeja jogar um fora; você vai, de qualquer maneira". Parece-me que você está fazendo exatamente isso. Dito isto, você deve escrever seus testes de unidade para testar a unidade de código e não uma grande faixa de código. Esses são testes mais funcionais e, portanto, devem ser aplicados a qualquer implementação interna.

Por exemplo, se eu quiser escrever um solucionador de PDE (equações diferenciais parciais), escreveria alguns testes tentando resolver coisas que posso resolver matematicamente. Esses são meus primeiros testes "unitários" - leia: testes funcionais executados como parte de uma estrutura xUnit. Isso não muda, dependendo do algoritmo usado para resolver o PDE. Tudo o que me importa é o resultado. Os segundos testes de unidade se concentrarão nas funções usadas para codificar o algoritmo e, portanto, seriam específicos do algoritmo - digamos Runge-Kutta. Se eu descobrisse que Runge-Kutta não era adequado, ainda teria os testes de nível superior (incluindo os que mostraram que Runge-Kutta não era adequado). Portanto, a segunda iteração ainda teria muitos dos mesmos testes que o primeiro.

Seu problema talvez seja no design e não necessariamente no código. Mas sem mais detalhes, é difícil dizer.

Sardathrion - Restabelecer Monica
fonte
É apenas periférico, mas o que é PDE?
um CVn
11
@ MichaelKjörling Eu acho que é a Equação Diferencial Parcial
09/02
2
Brooks não retirou essa afirmação em sua 2ª edição?
9116 Simon
Como assim você ainda terá os testes que mostram que Runge-Kutta não era adequado? Como são esses testes? Você quer dizer que você salvou o algoritmo Runge-Kutta que escreveu, antes de descobrir que não era adequado, e a execução dos testes de ponta a ponta com o RK no mix falharia?
Moteutsch
5

Você deve ter em mente que o TDD é um processo iterativo. Escreva um teste pequeno (na maioria dos casos, algumas linhas devem ser suficientes) e execute-o. O teste deve falhar, agora trabalhe diretamente na sua fonte principal e tente implementar a funcionalidade testada para que o teste seja aprovado. Agora comece de novo.

Você não deve tentar escrever todos os testes de uma só vez, porque, como você notou, isso não vai dar certo. Isso reduz o risco de desperdiçar seu tempo escrevendo testes que não serão usados.

BenR
fonte
11
Eu não acho que posso me explicar muito bem. Eu escrevo testes iterativamente. Foi assim que acabei com várias centenas de testes de código que de repente se tornaram redundantes.
9788 GazeboDevroy
11
Como acima - Eu acho que deveria ser pensado como "testes e código" em vez de "testes de código"
Murph
11
+1: "Você não deve tentar escrever todos os testes de uma só vez"
S.Lott
4

Acho que você mesmo disse: não tinha certeza da sua abordagem antes de começar a escrever todos os seus testes de unidade.

O que aprendi comparando os projetos de TDD da vida real com os quais trabalhei (não muitos, de fato, apenas 3 cobrindo 2 anos de trabalho) com o que aprendi teoricamente, é que o Teste Automatizado! = Teste de Unidade (sem, é claro, ser mutuamente) exclusivo).

Em outras palavras, o T no TDD não precisa ter um U com ele ... Ele é automatizado, mas é menos um teste de unidade (como nas classes e métodos de teste) do que um teste funcional automatizado: está no mesmo nível de granularidade funcional como a arquitetura em que você está trabalhando atualmente. Você inicia de alto nível, com poucos testes e apenas o quadro funcional e, eventualmente, acaba com milhares de UTs e todas as suas classes bem definidas em uma bela arquitetura ...

Os testes de unidade oferecem uma grande ajuda quando você trabalha em equipe, para evitar alterações no código, criando ciclos intermináveis ​​de bugs. Mas nunca escrevi algo tão preciso ao começar a trabalhar em um projeto, antes de ter pelo menos um POC de trabalho global para cada história de usuário.

Talvez seja apenas a minha maneira pessoal de fazer isso. Não tenho experiência suficiente para decidir do zero quais padrões ou estrutura meu projeto terá; portanto, não vou perder meu tempo escrevendo centenas de UTs desde o início ...

De maneira mais geral, a idéia de quebrar tudo e jogar tudo sempre estará lá. Por mais "contínuo" que possamos tentar ser com nossas ferramentas e métodos, às vezes a única maneira de combater a entropia é começar de novo. Mas o objetivo é que, quando isso acontecer, os testes automatizados e de unidade que você implementou, tornem o seu projeto já menos dispendioso do que se não existisse - e, se você encontrar o equilíbrio.

GFK
fonte
3
bem dito - é TDD, não UTDD
Steven A. Lowe
Excelente resposta. Na minha experiência com TDD, é importante que os testes escritos se concentrem nos comportamentos funcionais do software e afastem-se dos testes de unidade. É mais difícil pensar nos comportamentos que você precisa de uma classe, mas isso leva a interfaces limpas e potencialmente simplifica a implementação resultante (você não adiciona funcionalidades que realmente não precisa).
JohnTESlade
4
Como as pessoas que praticam TDD lidam adequadamente com essas situações?
  1. considerando quando prototipar vs quando codificar
  2. ao perceber que o teste de unidade não é o mesmo que TDD
  3. por escritos testes TDD para verificar um recurso / matéria, não uma unidade funcional

A fusão de testes de unidade com desenvolvimento orientado a testes é fonte de muita angústia e angústia. Então, vamos revisá-lo mais uma vez:

  • o teste de unidade se preocupa em verificar cada módulo e função individuais na implementação ; na UT, você verá uma ênfase em coisas como métricas e testes de cobertura de código que são executados muito rapidamente
  • o desenvolvimento orientado a testes se preocupa em verificar cada recurso / matéria nos requisitos ; no TDD, você verá uma ênfase em coisas como escrever o teste primeiro, garantir que o código escrito não exceda o escopo pretendido e refatorar para obter qualidade

Em resumo: o teste de unidade tem um foco de implementação, o TDD tem um foco de requisitos. Eles não são a mesma coisa.

Steven A. Lowe
fonte
"O TDD tem um foco nos requisitos" Eu discordo totalmente disso. Os testes que você escreve no TDD são testes de unidade. Eles fazem verificar cada função / método. TDD faz tem uma ênfase na cobertura de código e faz o cuidado sobre os testes que executam rapidamente (e que seria melhor fazer, desde que você executar os testes a cada 30 segundos ou assim). Talvez você estivesse pensando em ATDD ou BDD?
precisa
11
@ian31: exemplo perfeito de fusão UT e TDD. Deve discordar e encaminhá-lo para algum material de origem en.wikipedia.org/wiki/Test-driven_development - o objetivo dos testes é definir os requisitos de código . BDD é ótimo. Nunca ouvi falar do ATDD, mas, à primeira vista, parece como eu aplico a escala TDD .
Steven A. Lowe
Você pode perfeitamente usar o TDD para projetar código técnico que não esteja diretamente relacionado a um requisito ou história do usuário. Você encontrará inúmeros exemplos disso na web, em livros, conferências, inclusive de pessoas que iniciaram o TDD e o popularizaram. TDD é uma disciplina, uma técnica para escrever código, não deixará de ser TDD, dependendo do contexto em que você o usar.
precisa
Além disso, no artigo da Wikipedia que você mencionou: "Práticas avançadas de desenvolvimento orientado a testes podem levar ao ATDD, onde os critérios especificados pelo cliente são automatizados em testes de aceitação, que conduzem o processo tradicional de desenvolvimento orientado a testes unitários (UTDD). [ ...] Com o ATDD, a equipe de desenvolvimento agora tem um objetivo específico a ser cumprido, os testes de aceitação, que os mantém continuamente focados no que o cliente realmente deseja dessa história de usuário ". O que parece implicar que o ATDD se concentra principalmente nos requisitos, não no TDD (ou UTDD como eles colocam).
precisa
@ian31: A pergunta do OP sobre 'jogar várias centenas de testes de unidade' indicava uma confusão de escala. Você pode usar o TDD para construir um galpão, se quiser. : D
Steven A. Lowe
3

O desenvolvimento orientado a testes destina-se a impulsionar seu desenvolvimento. Os testes que você escreve o ajudam a afirmar a exatidão do código que está escrevendo no momento e aumentam a velocidade de desenvolvimento da primeira linha em diante.

Você parece acreditar que os testes são um fardo e só se destinam ao desenvolvimento incremental posteriormente. Essa linha de pensamento não está alinhada com o TDD.

Talvez você possa compará-lo com a digitação estática: embora seja possível escrever código usando nenhuma informação de tipo estático, a adição de tipo estático ao código ajuda a afirmar certas propriedades do código, liberando a mente e permitindo o foco em estruturas importantes, aumentando a velocidade e eficácia.

Dibbeke
fonte
2

O problema de fazer uma grande refatoração é que você pode e, às vezes, segue um caminho que o leva a perceber que você mordeu mais do que pode mastigar. Refatorações gigantes são um erro. Se o design do sistema é defeituoso em primeiro lugar, a refatoração pode levar você tão longe antes que você precise tomar uma decisão difícil. Deixe o sistema como está e resolva-o ou planeje redesenhar e fazer algumas mudanças importantes.

Existe, no entanto, outro caminho. O benefício real do código de refatoração é tornar as coisas mais simples, fáceis de ler e ainda mais fáceis de manter. Onde você aborda um problema sobre o qual você tem incerteza, gera uma mudança, vai tão longe para ver aonde ele pode levar a fim de aprender mais sobre o problema, depois joga fora o aumento e aplica uma nova refatoração com base no que o aumento ensinei você. O fato é que você realmente pode melhorar seu código apenas com certeza se as etapas forem pequenas e seus esforços de refatoração não ultrapassarem sua capacidade de escrever seus testes primeiro. A tentação é escrever um teste, depois codificar e depois codificar um pouco mais, porque uma solução pode parecer óbvia, mas logo você percebe que sua alteração mudará muitos outros testes; portanto, você deve tomar cuidado para mudar apenas uma coisa de cada vez.

A resposta, portanto, é nunca tornar sua refatoração maior. Passos de bebê. Comece extraindo métodos e tente remover a duplicação. Em seguida, passe para a extração de classes. Cada um em pequenos passos, uma pequena alteração de cada vez. Se você estiver extraindo código, escreva um teste primeiro. Se você estiver removendo o código, remova-o e execute seus testes, e decida se algum dos testes quebrados será mais necessário. Um pequeno passo de bebê de cada vez. Parece que levará mais tempo, mas reduzirá consideravelmente o tempo de refatoração.

A realidade é, no entanto, que cada pico é aparentemente um potencial desperdício de esforço. As alterações de código às vezes não levam a lugar algum, e você se restaura restaurando seu código de seus vcs. Esta é apenas uma realidade do que fazemos no dia a dia. Todo pico que falha não é desperdiçado, no entanto, se ele lhe ensina algo. Todo esforço de refatoração que falhar lhe ensinará que você está tentando fazer muito rapidamente ou que sua abordagem pode estar errada. Isso também não é perda de tempo, se você aprender algo com isso. Quanto mais você faz essas coisas, mais aprende e mais eficiente se tornará. Meu conselho é usá-lo por enquanto, aprender a fazer mais fazendo menos e aceitar que é assim que as coisas provavelmente precisam ser até que você melhore a identificação de quão longe deve dar um pico antes que ele não o leve a lugar nenhum.

S.Robins
fonte
1

Não tenho certeza sobre o motivo pelo qual sua abordagem foi falha após três dias. Dependendo das suas incertezas em sua arquitetura, você pode considerar mudar sua estratégia de teste:

  • Se você não tiver certeza sobre o desempenho, pode querer começar com alguns testes de integração que afirmam o desempenho?

  • Quando a complexidade da API é o que você está investigando, escreva alguns testes de unidade pequenos e reais para descobrir qual seria a melhor maneira de fazer isso. Não se importe em implementar nada, apenas faça com que suas classes retornem valores codificados ou faça com que eles jogem NotImplementedExceptions.

Boris Callens
fonte
0

Para mim, os testes de unidade também são uma ocasião para colocar a interface em uso "real" (bem, tão real quanto os testes de unidade!).

Se sou forçado a fazer um teste, tenho que exercitar meu design. Isso ajuda a manter as coisas sãs (se algo é tão complexo que escrever um teste é um fardo, como será usá-lo?).

Isso não evita alterações no design, mas expõe a necessidade delas. Sim, uma reescrita completa é uma dor. Para (tentar) evitá-lo, costumo configurar (um ou mais) protótipo, possivelmente em Python (com o desenvolvimento final em c ++).

É verdade que você nem sempre tem tempo para todos esses presentes. Esses são precisamente os casos em que você precisará de uma quantidade MAIOR de tempo para atingir seus objetivos ... e / ou manter tudo sob controle.

Francesco
fonte
0

Bem-vindo ao circo de desenvolvedores criativos .


Em vez de respeitar toda a maneira 'legal / razoável' de codificar no início,
tente a intuição , acima de tudo, se ela é importante e nova para você e se nenhuma amostra parecer que você deseja:

- Escreva com seu instinto, com coisas que você já conhece , não com sua mente e imaginação.
- E pare.
- Pegue uma lupa e inspecione todas as palavras que você escreve: Você escreve "texto" porque "texto" está próximo de String, mas é necessário "verbo", "adjetivo" ou algo mais preciso, leia novamente e ajuste o método com um novo sentido
. .. ou, você escreveu um pedaço de código pensando no futuro? remova-o
- Corrija, execute outra tarefa (esporte, cultura ou outras coisas fora dos negócios), volte e leia novamente.
- Tudo se encaixa bem,
- Corrija, execute outra tarefa, volte e leia novamente.
- Tudo se encaixa bem, passe para TDD
- Agora tudo está correto, bom
- Tente o benchmark para apontar as coisas a serem otimizadas, faça-o.

O que aparece:
- você escreveu um código respeitando todas as regras
- você obtém uma experiência, uma nova maneira de trabalhar,
- algo mudou em sua mente, você nunca terá medo pela nova configuração.

E agora, se você vir uma UML parecida com a acima, será capaz de dizer
"Chefe, eu começo pelo TDD para isso ...."
é outra coisa nova?
"Chefe, eu tentaria algo antes de decidir como codificarei .."

Atenciosamente PARIS
Claude

cl-r
fonte