Normalmente, tento seguir os conselhos do livro Trabalhando Efetivamente com o Legacy Cod e . Eu quebro dependências, movo partes do código para @VisibleForTesting public static
métodos e para novas classes para tornar o código (ou pelo menos parte dele) testável. E escrevo testes para garantir que não quebre nada ao modificar ou adicionar novas funções.
Um colega diz que eu não deveria fazer isso. Seu raciocínio:
- O código original pode não funcionar corretamente em primeiro lugar. E escrever testes para ele torna futuras correções e modificações mais difíceis, pois os desenvolvedores também precisam entender e modificar os testes.
- Se for um código GUI com alguma lógica (~ 12 linhas, 2-3 blocos if / else, por exemplo), um teste não vale a pena, pois o código é muito trivial para começar.
- Padrões ruins semelhantes também podem existir em outras partes da base de código (que ainda não vi, sou bastante nova); será mais fácil limpá-los todos em uma grande refatoração. Extrair a lógica pode minar essa possibilidade futura.
Devo evitar extrair peças testáveis e escrever testes se não tivermos tempo para refatoração completa? Existe alguma desvantagem nisso que eu deva considerar?
Respostas:
Aqui está minha impressão pessoal não científica: todas as três razões parecem ilusões cognitivas generalizadas, mas falsas.
fonte
Algumas reflexões:
Quando você está refatorando o código legado, não importa se alguns dos testes que você escreve contradizem as especificações ideais. O que importa é que eles testem o comportamento atual do programa . Refatorar consiste em executar pequenas etapas iso-funcionais para tornar o código mais limpo; você não deseja se envolver em correções de erros enquanto está refatorando. Além disso, se você detectar um bug flagrante, ele não será perdido. Você sempre pode escrever um teste de regressão para ele e desativá-lo temporariamente, ou inserir uma tarefa de correção de erros em sua lista de pendências para mais tarde. Uma coisa de cada vez.
Concordo que o código GUI puro é difícil de testar e talvez não seja adequado para a refatoração no estilo " Trabalhando Efetivamente ... ". No entanto, isso não significa que você não deve extrair um comportamento que não tem nada a ver na camada da GUI e testar o código extraído. E "12 linhas, 2-3 se / outro bloco" não é trivial. Todo o código com pelo menos um pouco de lógica condicional deve ser testado.
Na minha experiência, grandes refatorações não são fáceis e raramente funcionam. Se você não definir objetivos precisos e minúsculos, há um alto risco de embarcar em um retrabalho interminável e puxador de cabelos, onde nunca cairá de pé no final. Quanto maior a mudança, mais você corre o risco de quebrar algo e mais problemas terá para descobrir onde falhou.
Tornar as coisas progressivamente melhores com pequenas refatorações ad hoc não está "minando as possibilidades futuras", mas sim permitindo-as - solidificando o terreno pantanoso onde está a sua aplicação. Você definitivamente deveria fazê-lo.
fonte
Também re: "O código original pode não funcionar corretamente" - isso não significa que você apenas altera o comportamento do código sem se preocupar com o impacto. Outro código pode depender do que parece ser um comportamento quebrado ou efeitos colaterais da implementação atual. A cobertura de teste do aplicativo existente deve facilitar a refatoração mais tarde, porque ajudará você a descobrir quando você quebrou algo acidentalmente. Você deve testar as partes mais importantes primeiro.
fonte
A resposta de Kilian cobre os aspectos mais importantes, mas quero expandir os pontos 1 e 3.
Se um desenvolvedor deseja alterar (refatorar, estender, depurar) o código, ele precisa entender. Ela precisa garantir que suas alterações afetem exatamente o comportamento que ela deseja (nada no caso de refatoração) e nada mais.
Se houver testes, ela também precisará entender os testes, com certeza. Ao mesmo tempo, os testes devem ajudá-la a entender o código principal, e os testes são muito mais fáceis de entender do que o código funcional (a menos que sejam testes ruins). E os testes ajudam a mostrar o que mudou no comportamento do código antigo. Mesmo que o código original esteja errado e o teste teste esse comportamento errado, isso ainda é uma vantagem.
No entanto, isso exige que os testes sejam documentados como teste de comportamento preexistente, não uma especificação.
Algumas reflexões sobre o ponto 3 também: além do fato de que o "grande golpe" raramente acontece de verdade, há também outra coisa: na verdade, não é mais fácil. Para ser mais fácil, várias condições teriam que ser aplicadas:
XYZSingleton
? Seu getter de instância é sempre chamadogetInstance()
? E como você encontra suas hierarquias muito profundas? Como você procura seus objetos divinos? Isso requer análise de métricas de código e, em seguida, inspeção manual das métricas. Ou você simplesmente tropeça neles enquanto trabalha, como fez.fonte
Em algumas empresas, existe uma cultura em que eles são reticentes para permitir que os desenvolvedores a qualquer momento aprimorem o código que não fornece diretamente valor adicional, por exemplo, nova funcionalidade.
Provavelmente estou pregando aos convertidos aqui, mas isso é claramente uma economia falsa. O código limpo e conciso beneficia os desenvolvedores subsequentes. Só que o retorno não é imediatamente evidente.
Pessoalmente, assino o Princípio Escoteiro, mas outros (como você viu) não o fazem.
Dito isto, o software sofre entropia e acumula dívidas técnicas. Desenvolvedores anteriores com pouco tempo (ou talvez apenas preguiçosos ou inexperientes) podem ter implementado soluções de bugs abaixo do ideal em relação às bem projetadas. Embora possa parecer desejável refatorá-los, você corre o risco de introduzir novos bugs no código que está funcionando (para os usuários de qualquer maneira).
Algumas mudanças são de menor risco que outras. Por exemplo, onde trabalho, costuma haver muito código duplicado que pode ser desenvolvido com segurança em uma sub-rotina com impacto mínimo.
Por fim, é necessário fazer um julgamento sobre até que ponto você leva a refatoração, mas há um valor inegável em adicionar testes automatizados, se eles já não existirem.
fonte
Na minha experiência, um tipo de teste de caracterização funciona bem. Ele oferece uma cobertura de teste ampla, mas não muito específica, de forma relativamente rápida, mas pode ser difícil de implementar para aplicativos de GUI.
Em seguida, eu escrevia testes de unidade para as peças que você deseja alterar e o fazia toda vez que deseja fazer alterações, aumentando assim a cobertura do teste de unidade ao longo do tempo.
Essa abordagem fornece uma boa idéia se as mudanças estão afetando outras partes do sistema e permite que você faça as alterações necessárias mais rapidamente.
fonte
Re: "O código original pode não funcionar corretamente":
Os testes não são escritos em pedra. Eles podem ser alterados. E se você testou um recurso errado, seria fácil reescrever o teste mais corretamente. Somente o resultado esperado da função testada deve ter sido alterado, afinal.
fonte
Bem, sim. Respondendo como engenheiro de teste de software. Primeiro, você deve testar tudo o que faz. Porque se não, você não sabe se funciona ou não. Isso pode parecer óbvio para nós, mas tenho colegas que vêem de maneira diferente. Mesmo que seu projeto seja pequeno e nunca possa ser entregue, você precisa olhar o rosto do usuário e dizer que sabe que ele funciona porque você o testou.
Código não trivial sempre contém bugs (citar um cara da uni; e se não houver bugs, é trivial) e nosso trabalho é encontrá-los antes que o cliente o faça. O código herdado possui bugs herdados. Se o código original não funcionar da maneira que deveria, você quer saber, acredite. Os bugs estão ok se você os conhece, não tenha medo de encontrá-los, é para isso que servem as notas de versão.
Se bem me lembro, o livro Refatoração diz para testar constantemente de qualquer maneira., Portanto faz parte do processo.
fonte
Faça a cobertura do teste automatizado.
Cuidado com os desejos, tanto seus quanto dos seus clientes e chefes. Por mais que eu adorasse acreditar que minhas alterações estarão corretas na primeira vez e só precisarei testar uma vez, aprendi a tratar esse tipo de pensamento da mesma maneira que trato os e-mails fraudulentos nigerianos. Bem, principalmente; Eu nunca fui para um email fraudulento, mas recentemente (quando gritei) desisti de não usar as melhores práticas. Foi uma experiência dolorosa que se arrastou (cara) sem parar. Nunca mais!
Eu tenho uma citação favorita do gibi da web Freefall: "Você já trabalhou em um campo complexo em que o supervisor tem apenas uma idéia aproximada dos detalhes técnicos? ... Então você sabe que a maneira mais segura de fazer com que o supervisor falhe é: siga todas as suas ordens sem questionar. "
Provavelmente, é apropriado limitar a quantidade de tempo que você investe.
fonte
Se você estiver lidando com grandes quantidades de código legado que não está atualmente em teste, obter a cobertura do teste agora, em vez de esperar por uma grande reescrita hipotética no futuro, é a decisão certa. Começar escrevendo testes de unidade não é.
Sem testes automatizados, depois de fazer alterações no código, você precisa fazer alguns testes manuais de ponta a ponta do aplicativo para garantir que ele funcione. Comece escrevendo testes de integração de alto nível para substituir isso. Se seu aplicativo lê arquivos, os valida, processa os dados de alguma maneira e exibe os resultados desejados para os testes que capturam tudo isso.
Idealmente, você terá dados de um plano de teste manual ou poderá obter uma amostra dos dados de produção reais para usar. Caso contrário, como o aplicativo está em produção, na maioria dos casos, ele está fazendo o que deveria ser; então, crie dados que atingirão todos os pontos altos e assumirão que a saída está correta por enquanto. Não é pior do que assumir uma função pequena, supondo que esteja fazendo o que o nome ou qualquer comentário sugere, e escrevendo testes assumindo que está funcionando corretamente.
Depois que você tiver o suficiente desses testes de alto nível escritos para capturar a operação normal dos aplicativos e os casos de erro mais comuns, a quantidade de tempo que você precisará gastar no teclado para tentar detectar erros do código, fazendo algo diferente do que você pensou que deveria fazer isso diminuirá significativamente, facilitando a refatoração futura (ou mesmo uma grande reescrita).
Como você é capaz de expandir a cobertura dos testes de unidade, pode reduzir ou até retirar a maioria dos testes de integração. Se o aplicativo estiver lendo / gravando arquivos ou acessando um banco de dados, testando essas partes isoladamente e zombando delas ou iniciando seus testes criando as estruturas de dados lidas no arquivo / banco de dados, é um ponto óbvio para começar. Criar essa infraestrutura de teste levará muito mais tempo do que escrever um conjunto de testes rápidos e sujos; e toda vez que você executa um conjunto de 2 minutos de testes de integração, em vez de gastar 30 minutos testando manualmente uma fração do que os testes de integração cobriram, você já está obtendo uma grande vitória.
fonte