Em outra pergunta, foi revelado que uma das dificuldades do TDD é manter o conjunto de testes sincronizado com a base de código durante e após a refatoração.
Agora, sou um grande fã de refatoração. Eu não vou desistir de fazer TDD. Mas também experimentei os problemas dos testes escritos de tal maneira que a refatoração menor leva a muitas falhas no teste.
Como você evita interromper os testes ao refatorar?
- Você escreve os testes 'melhor'? Se sim, o que você deve procurar?
- Você evita certos tipos de refatoração?
- Existem ferramentas de refatoração de teste?
Edit: Eu escrevi uma nova pergunta que perguntou o que eu queria perguntar (mas manteve essa como uma variante interessante).
development-process
tdd
refactoring
Alex Feinman
fonte
fonte
Respostas:
O que você está tentando fazer não é realmente refatorar. Com a refatoração, por definição, você não muda o que seu software faz, mas como ele o faz.
Comece com todos os testes verdes (todos aprovados) e faça as modificações "sob o capô" (por exemplo, mova um método de uma classe derivada para a base, extraia um método ou encapsule um Composite com um Builder , etc.). Seus testes ainda devem passar.
O que você está descrevendo parece não ser refatoração, mas uma reformulação, que também aumenta a funcionalidade do seu software em teste. TDD e refatoração (como tentei defini-lo aqui) não estão em conflito. Você ainda pode refatorar (verde-verde) e aplicar TDD (vermelho-verde) para desenvolver a funcionalidade "delta".
fonte
Um dos benefícios de ter testes de unidade é para que você possa refatorar com confiança.
Se a refatoração não alterar a interface pública, você deixa os testes de unidade como estão e garante, após a refatoração, que todos passem.
Se a refatoração alterar a interface pública, os testes deverão ser reescritos primeiro. Refatorar até que os novos testes sejam aprovados.
Eu nunca evitaria qualquer refatoração porque interrompe os testes. Escrever testes de unidade pode ser uma dor de cabeça, mas vale a pena a longo prazo.
fonte
Ao contrário das outras respostas, é importante observar que algumas formas de teste podem se tornar frágeis quando o sistema em teste (SUT) é refatorado, se o teste for uma caixa branca.
Se eu estiver usando uma estrutura de simulação que verifique a ordem dos métodos chamados nas zombarias (quando a ordem é irrelevante porque as chamadas são livres de efeitos colaterais); se meu código for mais limpo com essas chamadas de método em uma ordem diferente e eu refatorar, meu teste será interrompido. Em geral, as zombarias podem introduzir fragilidade nos testes.
Se eu estiver verificando o estado interno do meu SUT, expondo seus membros privados ou protegidos (poderíamos usar "friend" no visual basic ou escalar o nível de acesso "internal" e usar "internalsvisibleto" em c #; em muitos idiomas OO, incluindo c # uma " subclasse específica de teste " pode ser usada) e, de repente, o estado interno da classe será importante - você pode refatorar a classe como uma caixa preta, mas os testes da caixa branca falharão. Suponha que um único campo seja reutilizado para significar coisas diferentes (não é uma boa prática!) Quando o SUT muda de estado - se o dividirmos em dois campos, talvez seja necessário reescrever testes quebrados.
As subclasses específicas de teste também podem ser usadas para testar métodos protegidos - o que pode significar que um refator do ponto de vista do código de produção é uma mudança radical do ponto de vista do código de teste. Mover algumas linhas para dentro ou fora de um método protegido pode não ter efeitos colaterais na produção, mas interromper um teste.
Se eu usar " ganchos de teste " ou qualquer outro código de compilação condicional ou específico do teste, pode ser difícil garantir que os testes não sejam interrompidos devido a dependências frágeis da lógica interna.
Portanto, para evitar que os testes sejam acoplados aos detalhes internos íntimos do SUT, isso pode ajudar a:
Todos os pontos acima são exemplos de acoplamento de caixa branca usado em testes. Portanto, para evitar completamente a refatoração dos testes de quebra, use o teste de caixa preta do SUT.
Isenção de responsabilidade: Com o objetivo de discutir a refatoração aqui, estou usando a palavra um pouco mais amplamente para incluir alterações na implementação interna sem efeitos externos visíveis. Alguns puristas podem discordar e se referir exclusivamente ao livro Refatoração de Martin Fowler e Kent Beck - que descreve operações de refatoração atômica.
Na prática, tendemos a tomar etapas ininterruptas um pouco maiores do que as operações atômicas descritas lá e, em particular, alterações que deixam o código de produção se comportando de forma idêntica do lado de fora podem não deixar os testes aprovados. Mas acho justo incluir "um algoritmo substituto para outro algoritmo que tenha comportamento idêntico" como um refator, e acho que Fowler concorda. O próprio Martin Fowler diz que a refatoração pode quebrar os testes:
fonte
Se seus testes são interrompidos quando você está refatorando, então você não está, por definição, refatorando, que está "alterando a estrutura do seu programa sem alterar o comportamento do seu programa".
Às vezes, você precisa alterar o comportamento de seus testes. Talvez você precise mesclar dois métodos juntos (digamos, bind () e listen () em uma classe de soquete TCP de escuta), para que outras partes do seu código tentem e falhem ao usar a API agora alterada. Mas isso não é refatoração!
fonte
Penso que o problema com esta pergunta é que pessoas diferentes estão adotando a palavra 'refatoração' de maneira diferente. Eu acho que é melhor definir cuidadosamente algumas coisas que você provavelmente quer dizer:
Como uma outra pessoa já observou, se você mantém a API igual e todos os seus testes de regressão operam na API pública, você não deve ter problemas. A refatoração não deve causar problemas. Qualquer teste que falhou, significa que seu código antigo teve um erro e seu teste está com defeito ou seu novo código tem um erro.
Mas isso é bastante óbvio. Então, provavelmente, você quer dizer com refatoração que está alterando a API.
Então, deixe-me responder como abordar isso!
Primeiro, crie uma NEW API, que faça o que você deseja que seja o seu comportamento da NEW API. Se essa nova API tiver o mesmo nome que uma API ANTIGA, anexarei o nome _NEW ao novo nome da API.
int DoSomethingInterestingAPI ();
torna-se:
OK - nesse estágio - todos os seus testes de regressão ainda passam - usando o nome DoSomethingInterestingAPI ().
NEXT, consulte seu código e altere todas as chamadas para DoSomethingInterestingAPI () para a variante apropriada de DoSomethingInterestingAPI_NEW (). Isso inclui atualizar / reescrever as partes de seus testes de regressão que precisam ser alteradas para usar a nova API.
NEXT, marque DoSomethingInterestingAPI_OLD () como [[obsoleto ()]]. Mantenha a API reprovada pelo tempo que desejar (até atualizar com segurança todo o código que possa depender dela).
Com essa abordagem, quaisquer falhas nos testes de regressão são simplesmente erros nesse teste de regressão ou identificam erros no seu código - exatamente como você deseja. Esse processo faseado de revisar uma API criando explicitamente as versões _NEW e _OLD da API permite que você coexista por um tempo bits do código novo e antigo.
fonte
Eu suponho que seus testes de unidade são de uma granularidade que eu chamaria de "estúpido" :) ou seja, eles testam as minúcias absolutas de cada classe e função. Afaste-se das ferramentas do gerador de código e escreva testes que se aplicam a uma superfície maior, para refatorar os internos o quanto quiser, sabendo que as interfaces para seus aplicativos não foram alteradas e seus testes ainda funcionam.
Se você deseja ter testes de unidade que testam todos os métodos, espere ter que refatorá-los ao mesmo tempo.
fonte
O que dificulta é o acoplamento . Todos os testes vêm com algum grau de acoplamento aos detalhes da implementação, mas os testes de unidade (independentemente de serem TDD ou não) são especialmente ruins porque interferem com os internos: mais testes de unidade equivalem a mais código acoplado a unidades, ou seja, assinaturas de métodos / qualquer outra interface pública de unidades - pelo menos.
"Unidades", por definição, são detalhes de implementação de baixo nível; a interface das unidades pode e deve mudar / dividir / mesclar e, de outra forma, sofrer alterações à medida que o sistema evolui. A abundância de testes de unidade pode realmente impedir essa evolução mais do que ajuda.
Como evitar a quebra de testes ao refatorar? Evite o acoplamento. Na prática, significa evitar o máximo de testes de unidade possível e preferir testes de nível / integração mais agnósticos nos detalhes da implementação. Lembre-se, porém, de que não existe uma bala de prata, os testes ainda precisam se encaixar em algum nível, mas, idealmente, deve ser uma interface explicitamente versionada usando o Semantic Versioning, ou seja, geralmente no nível de API / aplicativo publicado (você não quer fazer o SemVer para cada unidade em sua solução).
fonte
Seus testes estão muito acoplados à implementação e não ao requisito.
considere escrever seus testes com comentários como este:
Dessa forma, você não pode refatorar o significado dos testes.
fonte