ti-enxame.com

Como você mantém seus testes de unidade funcionando ao refatorar?

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 uma 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).

33
Alex Feinman

O que você está tentando fazer não é realmente refatorar. Com a refatoração, por definição, você não altera o que o seu software faz, você altera como 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 Construtor , 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".

38
azheglov

Um dos benefícios de ter testes de unidade é que você pode 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.

21
Tim Murphy

Ao contrário das outras respostas, é importante observar que algumas formas de teste podem tornar-se frágeis quando o sistema em teste (SUT) é refatorado ( se o teste for uma caixa branca.

Se estou usando uma estrutura de simulação que verifica a ordem dos métodos chamados nas zombarias (quando a ordem é irrelevante porque as chamadas têm efeito colateral) livre); 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 estou 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 OO idiomas, incluindo c # a " subclasse de teste específico " poderia ser usado), 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 de 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:

  • Use stubs em vez de zombarias, sempre que possível. Para mais informações, consulte blog de Fabio Periera sobre testes tautológicos e meu blog sobre testes tautológicos .
  • Se estiver usando zombarias, evite verificar a ordem dos métodos chamados, a menos que seja importante.
  • Tente evitar verificar o estado interno do seu SUT - use sua API externa, se possível.
  • Tente evitar lógica específica de teste no código de produção
  • Tente evitar o uso de subclasses específicas de teste.

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: Para discutir a refatoração aqui, estou usando o Word 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 passarem. Mas acho justo incluir "um algoritmo substituto para outro algoritmo que tenha comportamento idêntico" como refatorador, e acho que Fowler concorda. O próprio Martin Fowler diz que a refatoração pode quebrar os testes:

Ao escrever um teste de simulação, você está testando as chamadas de saída do SUT para garantir que ele fale adequadamente com seus fornecedores. Um teste clássico se preocupa apenas com o estado final - não como esse estado foi derivado. Os testes mockistas são, portanto, mais acoplados à implementação de um método. Mudar a natureza das chamadas para colaboradores geralmente causa um teste de simulação.

[...]

O acoplamento à implementação também interfere na refatoração, pois as alterações na implementação têm muito mais probabilidade de interromper os testes do que nos testes clássicos.

Fowler - Zombarias não são stubs

10
perfectionist

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 escuta TCP)], para que outras partes do seu código tentem e falhem ao usar o now API alterada, mas isso não é refatoração!

5
Frank Shearar

Penso que o problema desta 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:

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

Como 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á ruim 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:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

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 obsoleta 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.

4
Lewis Pringle

mantendo o conjunto de testes sincronizado com a base de código durante e após a refatoração

O que dificulta é 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 associado 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, isso 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 da API/aplicativo publicado (você não quer fazer o SemVer para cada unidade em sua solução).

1
KolA

Eu suponho que seus testes de unidade sejam de granularidade que eu chamaria de "estúpidos" :) ou seja, eles testam as minúcias absolutas de cada classe e função. Afaste-se das ferramentas geradoras 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.

1
gbjbaanb

Seus testes estão muito acoplados à implementação e não ao requisito.

considere escrever seus testes com comentários como este:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

dessa forma, você não pode refatorar o significado dos testes.

0
mcintyre321