Estou refatorando uma enorme classe de código legado. A refatoração (presumo) defende isso:
- escreva testes para a classe herdada
- refatorar o pedaço fora da classe
Problema: depois que refatorar a classe, meus testes na etapa 1 precisarão ser alterados. Por exemplo, o que antes era um método legado, agora pode ser uma classe separada. O que era um método agora pode ser de vários métodos. Todo o cenário da classe herdada pode ser destruído em algo novo e, portanto, os testes que escrevo na etapa 1 serão quase nulos. Em essência, adicionarei a Etapa 3. reescrever profusamente meus testes
Qual é o objetivo então escrever testes antes de refatorar? Parece mais um exercício acadêmico de criar mais trabalho para mim. Estou escrevendo testes para o método agora e estou aprendendo mais sobre como testar as coisas e como o método legado funciona. Pode-se aprender isso apenas lendo o próprio código legado, mas escrever testes é quase como esfregar meu nariz nele e também documentar esse conhecimento temporário em testes separados. Portanto, dessa maneira, quase não tenho escolha a não ser aprender o que o código está fazendo. Eu disse temporariamente aqui, porque refatorarei o código e toda a minha documentação e testes serão nulos e sem efeito por uma parte significativa, exceto que meu conhecimento permanecerá e permitirá que eu seja mais atualizado na refatoração.
Essa é a verdadeira razão para escrever testes antes de refatorar - para me ajudar a entender melhor o código? Tem que haver outra razão!
Por favor explique!
Nota:
Existe este post: faz sentido escrever testes para código legado quando não há tempo para uma refatoração completa? mas diz "escrever testes antes do refator", mas não diz "por que" ou o que fazer se "escrever testes" parecer "trabalho ocupado que será destruído em breve"
fonte
Respostas:
Refatorar é limpar um pedaço de código (por exemplo, melhorar o estilo, o design ou os algoritmos), sem alterar o comportamento (visível externamente). Você escreve testes para não se certificar de que o código antes e depois da refatoração é o mesmo; em vez disso, você escreve testes como um indicador de que seu aplicativo antes e depois da refatoração se comporta da mesma maneira: O novo código é compatível e nenhum novo erro foi introduzido.
Sua principal preocupação deve ser escrever testes de unidade para a interface pública do seu software. Essa interface não deve ser alterada; portanto, os testes (que são uma verificação automática dessa interface) também não devem ser alterados.
No entanto, os testes também são úteis para localizar erros, portanto, pode fazer sentido escrever testes para partes particulares do seu software. Espera-se que esses testes sejam alterados durante a refatoração. Se você deseja alterar um detalhe de implementação (como a nomeação de uma função privada), primeiro atualiza os testes para refletir suas expectativas alteradas e, em seguida, verifique se o teste falha (suas expectativas não são atendidas) e depois altera o código real e verifique se todos os testes passam novamente. Em nenhum momento os testes para a interface pública devem começar a falhar.
Isso é mais difícil ao realizar alterações em uma escala maior, por exemplo, redesenhando várias partes dependentes de código. Mas haverá algum tipo de limite e, nesse limite, você poderá escrever testes.
fonte
Ah, mantendo sistemas legados.
Idealmente, seus testes tratam a classe apenas por meio de sua interface com o restante da base de código, outros sistemas e / ou interface do usuário. Interfaces. Você não pode refatorar a interface sem afetar os componentes upstream ou downstream. Se é tudo uma bagunça fortemente acoplada, é melhor considerar o esforço de reescrever em vez de refatorar, mas é em grande parte semântica.
Edit: Digamos que parte do seu código mede algo e tem uma função que simplesmente retorna um valor. A única interface está chamando a função / método / outros enfeites e recebendo o valor retornado. É um acoplamento frouxo e fácil de testar na unidade. Se o seu programa principal possui um subcomponente que gerencia um buffer, e todas as chamadas para ele dependem do próprio buffer, de algumas variáveis de controle e retrocede mensagens de erro por outra seção do código, você pode dizer que está bem acoplado e teste de unidade difícil. Você ainda pode fazer isso com quantidades suficientes de objetos simulados e outros enfeites, mas fica confuso. Especialmente em c. Qualquer quantidade de refatoração de como o buffer funciona interromperá o subcomponente.
End Edit
Se você estiver testando sua classe através de interfaces que permanecem estáveis, seus testes deverão ser válidos antes e depois da refatoração. Isso permite que você faça alterações com confiança de que não a quebrou. Pelo menos, mais confiança.
Também permite fazer alterações incrementais. Se esse é um projeto grande, acho que você não quer simplesmente desmembrar tudo, criar um sistema totalmente novo e começar a desenvolver testes. Você pode alterar uma parte, testá-lo e garantir que a alteração não derrube o resto do sistema. Ou, se houver, você pode pelo menos ver a bagunça gigante emaranhada se desenvolvendo, em vez de ser surpreendida por ela ao soltar.
Embora você possa dividir um método em três, eles ainda farão a mesma coisa que o método anterior, para que você possa fazer o teste do método antigo e dividi-lo em três. O esforço de escrever o primeiro teste não é desperdiçado.
Além disso, tratar o conhecimento do sistema legado como "conhecimento temporário" não está indo bem. Saber como era antes é essencial quando se trata de sistemas legados. Muito útil para a velha questão de "por que diabos isso acontece?"
fonte
Minha própria resposta / realização:
Ao corrigir vários erros durante a refatoração, percebo que não teria feito o código se mover tão facilmente sem ter testes. Os testes me alertam sobre as "diferenças" comportamentais / funcionais que apresento alterando meu código.
Você não precisa estar hiperconsciente quando tiver bons testes. Você pode editar seu código de maneira mais descontraída. Os testes fazem as verificações e verificações de sanidade para você.
Além disso, meus testes permaneceram praticamente os mesmos que eu refatorei e não foram destruídos. Na verdade, notei algumas oportunidades extras para adicionar asserções aos meus testes à medida que me aprofundava no código.
ATUALIZAR
Bem, agora estou alterando bastante meus testes: / Como refatorei a função original (removi a função e criei uma nova classe de limpador, movendo o cotão que costumava estar dentro da função fora da nova classe), agora o código em teste que eu executei antes leva parâmetros diferentes sob um nome de classe diferente e produz resultados diferentes (o código original com o cotão tinha mais resultados para testar). E assim meus testes precisam refletir essas alterações e basicamente estou reescrevendo meus testes em algo novo.
Suponho que existem outras soluções que posso fazer para evitar a reescrita de testes. ou seja, mantenha o nome da função antiga com o novo código e o cotão dentro dele ... mas não sei se é a melhor ideia e ainda não tenho muita experiência para julgar o que fazer.
fonte
Use seus testes para direcionar seu código ao fazê-lo. No código legado, isso significa escrever testes para o código que você vai alterar. Dessa forma, eles não são um artefato separado. Os testes devem ser sobre o que o código precisa alcançar e não sobre o interior de como ele o faz.
Geralmente, você deseja adicionar testes ao código que não possui nenhum) para o código que você vai refatorar para garantir que o comportamento dos códigos continue funcionando conforme o esperado. Assim, a execução contínua do conjunto de testes durante a refatoração é uma rede de segurança fantástica. O pensamento de alterar o código sem um conjunto de testes para confirmar que as mudanças não estão afetando algo imprevisto é assustador.
Quanto ao âmago da questão de atualizar testes antigos, escrever novos testes, excluir testes antigos etc. Apenas vejo isso como parte do custo do desenvolvimento de software profissional moderno.
fonte
Qual é o objetivo da refatoração no seu caso específico?
Suponhamos, com o objetivo de responder à minha resposta, que todos acreditamos (até certo ponto) no TDD (Test-Driven Development).
Se o objetivo da sua refatoração é limpar o código existente sem alterar o comportamento existente, escrever testes antes da refatoração é como garantir que você não alterou o comportamento do código, se for bem-sucedido, os testes serão bem-sucedidos antes e depois você refatorar.
Os testes ajudarão você a garantir que seu novo trabalho realmente funcione.
Os testes provavelmente também descobrirão casos em que o trabalho original não funciona.
Mas como você realmente faz uma refatoração significativa sem afetar o comportamento até certo ponto?
Aqui está uma pequena lista de algumas coisas que podem acontecer durante a refatoração:
Vou argumentar que cada uma dessas atividades listadas muda o comportamento de alguma maneira.
E vou argumentar que, se sua refatoração mudar o comportamento, seus testes ainda serão da maneira que você garantirá que não quebrou nada.
Talvez o comportamento não mude no nível da macro, mas o objetivo do teste de unidade não é garantir o comportamento da macro. Isso é teste de integração . O objetivo do teste de unidade é garantir que os bits e partes individuais dos quais você constrói seu produto não sejam quebrados. Corrente, elo mais fraco, etc.
Que tal esse cenário:
Presuma que você tem
function bar()
function foo()
faz uma chamada parabar()
function flee()
também faz uma chamada para funcionarbar()
Só por variedade,
flam()
faz uma ligação parafoo()
Tudo funciona esplendidamente (aparentemente, pelo menos).
Você refatora ...
bar()
é renomeado parabarista()
flee()
é alterado para chamarbarista()
foo()
não é alterado para chamarbarista()
Obviamente, seus testes para ambos
foo()
eflam()
agora falham.Talvez você não tenha percebido o
foo()
chamadobar()
em primeiro lugar. Você certamente não percebeu que issoflam()
dependiabar()
por meio defoo()
.Tanto faz. O ponto é que seus testes descobrirão o comportamento recentemente quebrado de ambos
foo()
eflam()
, de maneira incremental, durante o trabalho de refatoração.Os testes acabam ajudando você a refatorar bem.
A menos que você não tenha nenhum teste.
Esse é um exemplo artificial. Há quem argumente que, se mudar os
bar()
intervalosfoo()
,foo()
era muito complexo para começar e deveria ser dividido. Mas os procedimentos podem chamar outros procedimentos por um motivo e é impossível eliminar toda a complexidade, certo? Nosso trabalho é gerenciar razoavelmente bem a complexidade.Considere outro cenário.
Você está construindo um edifício.
Você constrói um andaime para ajudar a garantir que o edifício seja construído corretamente.
O andaime ajuda a construir um poço de elevador, entre outras coisas. Depois, você derruba o andaime, mas o poço do elevador permanece. Você destruiu o "trabalho original" destruindo os andaimes.
A analogia é tênue, mas o ponto é que não é inédito criar ferramentas para ajudá-lo a criar produtos. Mesmo que as ferramentas não sejam permanentes, elas são úteis (até necessárias). Carpinteiros fazem gabaritos o tempo todo, às vezes apenas para um emprego. Depois eles separam os gabaritos, às vezes usando as peças para construir outros gabaritos para outros trabalhos, às vezes não. Mas isso não torna os gabaritos inúteis ou desperdiçados.
fonte