Faz sentido escrever testes para código legado quando não há tempo para uma refatoração completa?

72

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 staticmé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?

is4
fonte
29
Parece que seu colega está apenas apresentando desculpas porque não funciona dessa maneira. As pessoas às vezes se comportam assim por serem tenazes demais para mudar sua maneira adotada de fazer as coisas.
Doc Brown
3
o que deve ser classificado como um bug pode ser invocado por outras partes do código transformando-o em um recurso
catraca aberração
11
O único bom argumento contra o qual consigo pensar é que sua própria refatoração pode introduzir novos erros se você interpretou mal / copiou algo. Por esse motivo, estou livre para refatorar e corrigir o conteúdo do meu coração na versão atualmente em desenvolvimento - mas quaisquer correções nas versões anteriores enfrentam um obstáculo muito maior e podem não ser aprovadas se forem "apenas" limpeza cosmética / estrutural desde considera-se que o risco excede o ganho potencial. Conheça a sua cultura local - não apenas a idéia de um coworker - e tenha EXTREMAMENTE razões fortes antes de fazer qualquer outra coisa.
Kevlam
6
O primeiro ponto é meio hilário - "Não teste, pode ser um buggy". Bem, sim? Então é bom saber disso - ou queremos consertar isso ou não queremos que ninguém mude o comportamento real para o que algumas especificações de design disseram. De qualquer maneira, testar (e executar os testes em um sistema automatizado) é benéfico.
Christopher Creutzig
3
Muitas vezes, a "única grande refatoração" que está prestes a acontecer e que curará todos os males é um mito, inventado por aqueles que simplesmente querem empurrar coisas que consideram chatas (escrever testes) para um futuro distante. E se alguma vez se tornar real, eles se arrependerão seriamente de ter deixado que se tornasse tão grande!
Julia Hayward

Respostas:

100

Aqui está minha impressão pessoal não científica: todas as três razões parecem ilusões cognitivas generalizadas, mas falsas.

  1. Claro, o código existente pode estar errado. Também pode estar certo. Como o aplicativo como um todo parece ter valor para você (caso contrário, você simplesmente o descartaria), na ausência de informações mais específicas, você deve assumir que ele é predominantemente correto. "Escrever testes torna as coisas mais difíceis porque há mais código envolvido no geral" é uma atitude simplista e muito errada.
  2. Por todos os meios, dedique seus esforços de refatoração, teste e aprimoramento nos locais em que eles agregam mais valor e menos esforço. As sub-rotinas da GUI de formatação de valor geralmente não são a primeira prioridade. Mas não testar algo porque "é simples" também é uma atitude muito errada. Praticamente todos os erros graves são cometidos porque as pessoas pensam que entenderam algo melhor do que realmente entenderam.
  3. "Vamos fazer tudo de uma só vez no futuro" é um bom pensamento. Geralmente, a grande investida permanece firme no futuro, enquanto no presente nada acontece. Eu, sou firmemente da convicção "lenta e constante vence a corrida".
Kilian Foth
fonte
23
+1 em "Praticamente todos os erros graves são cometidos porque as pessoas pensam que entenderam algo melhor do que realmente entenderam".
rem
Relativamente ao ponto 1 - com BDD , os testes são auto documentando ...
Robbie Dee
2
Como @ guillaume31 aponta, parte do valor de escrever testes está demonstrando como o código realmente funciona - o que pode ou não estar de acordo com as especificações. Mas pode ser a especificação que está "errada": as necessidades da empresa podem ter mudado e o código reflete os novos requisitos, mas a especificação não. Simplesmente assumir que o código está "errado" é excessivamente simplista (consulte o ponto 1). E, novamente, os testes dirão o que o código realmente faz, não o que alguém pensa / diz que faz (consulte o ponto 2).
David
mesmo se você der uma guinada, precisará entender o código. Os testes ajudarão você a detectar um comportamento inesperado, mesmo se você não refatorar, mas reescrever (e se você refatorar, eles ajudam a garantir que sua refatoração não viole o comportamento herdado - ou apenas onde você deseja que ele seja danificado). Sinta-se livre para incorporar ou não - como desejar.
Frank Hopkins
50

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.

guillaume31
fonte
5
+1 para "testes que você escreve testam o comportamento atual do programa "
David
17

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.

Rory Hunter
fonte
Triste verdade. Temos alguns bugs óbvios que se manifestam em casos extremos que não podemos corrigir, porque nosso cliente prefere consistência ao invés de correção. (Eles são causados devido ao código de coleta de dados permitindo que as coisas o código de comunicação não leva em conta, como deixar um campo em uma série de campos em branco)
Izkata
14

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:

  • O antipadrão a ser refatorado precisa ser facilmente encontrado. Todos os seus singletons são nomeados XYZSingleton? Seu getter de instância é sempre chamado getInstance()? 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.
  • A refatoração precisa ser mecânica. Na maioria dos casos, a parte mais difícil da refatoração é entender o código existente suficientemente bem para saber como alterá-lo. Singletons novamente: se o singleton se foi, como você obtém as informações necessárias para seus usuários? Geralmente significa entender o gráfico de chamada local para que você saiba de onde obter as informações. Agora, o que é mais fácil: pesquisar os dez singletons em seu aplicativo, entender os usos de cada um (o que leva à necessidade de entender 60% da base de código) e rasgá-los? Ou pegando o código que você já entende (porque você está trabalhando nele agora) e copiando os singletons que estão sendo usados ​​lá fora? Se a refatoração não é tão mecânica que requer pouco ou nenhum conhecimento do código circundante, não há utilidade em agrupá-lo.
  • A refatoração precisa ser automatizada. Isso é um pouco baseado em opiniões, mas aqui vai. Um pouco de refatoração é divertido e gratificante. Muita refatoração é entediante e chata. Deixar o código em que você acabou de trabalhar em um estado melhor oferece uma sensação agradável e calorosa antes de passar para coisas mais interessantes. Tentar refatorar uma base de código inteira o deixará frustrado e irritado com os programadores idiotas que a escreveram. Se você deseja fazer uma grande refatoração rápida, ela precisa ser amplamente automatizada para minimizar a frustração. Isto é, de certa forma, uma mistura dos dois primeiros pontos: você só pode automatizar a refatoração se conseguir encontrar o código incorreto (ou seja, facilmente encontrado) e alterar automaticamente (ou seja, mecânico).
  • A melhoria gradual contribui para um melhor caso de negócios. A grande refatoração rápida é incrivelmente perturbadora. Se você refatorar um pedaço de código, invariavelmente entrará em conflito de fusão com outras pessoas que trabalham nele, porque você acabou de dividir o método que eles estavam mudando em cinco partes. Ao refatorar um pedaço de código de tamanho razoável, você entra em conflito com algumas pessoas (1-2 ao dividir a megafunção de 600 linhas, 2-4 ao dividir o objeto god, 5 ao extrair o singleton de um módulo ), mas você teria esses conflitos de qualquer maneira por causa de suas edições principais. Quando você faz uma refatoração em toda a base de código, entra em conflito com todos. Sem mencionar que ele liga alguns desenvolvedores por dias. A melhoria gradual faz com que cada modificação do código demore um pouco mais. Isso o torna mais previsível e não existe um período de tempo tão visível quando nada acontece, exceto a limpeza.
Sebastian Redl
fonte
12

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.

Robbie Dee
fonte
2
Eu concordo totalmente em princípio, mas em muitas empresas tudo se resume a tempo e dinheiro. Se a parte "arrumar" leva apenas alguns minutos, tudo bem, mas quando a estimativa para a arrumação começar a ficar maior (para alguma definição de grande), você, a pessoa que codifica, precisará delegar essa decisão ao seu chefe ou gestor de projeto. Não cabe a você decidir o valor desse tempo gasto. Trabalhar na correção de bug X ou no novo recurso Y pode ter um valor muito maior para o projeto / empresa / cliente.
ozz
2
Você também pode não estar ciente de problemas maiores, como o projeto sendo descartado em 6 meses, ou simplesmente que a empresa valoriza seu tempo mais (por exemplo, você faz algo que eles consideram mais importante e outra pessoa começa a fazer o trabalho de reforma). O trabalho de refatoração também pode afetar os testes. Uma refatoração grande acionará uma regressão completa do teste? A empresa possui recursos que pode implantar para fazer isso?
ozz
Sim, conforme você tocou, existem inúmeras razões pelas quais uma cirurgia de código importante pode ou não ser uma boa idéia: outras prioridades de desenvolvimento, a vida útil do software, recurso de teste, experiência do desenvolvedor, acoplamento, ciclo de lançamento, familiaridade com o código base, documentação, criticidade missão, cultura da empresa etc etc etc. é um julgamento
Robbie Dee
4

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.

jamesj
fonte
3

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.

rem
fonte
11
Na IMO, testes individuais devem ser escritos em pedra, pelo menos até que o recurso que eles estão testando esteja acabado. São eles que verificam o comportamento do sistema existente e ajudam a garantir aos mantenedores que suas alterações não quebrarão o código legado que já pode confiar nesse comportamento. Altere os testes para um recurso ao vivo e você está removendo essas garantias.
cHao 06/02
3

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.

RedSonja
fonte
3

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.

Technophile
fonte
1

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.

IntegrationTestCase1()
{
    var input = ReadDataFile("path\to\test\data\case1in.ext");
    bool validInput = ValidateData(input);
    Assert.IsTrue(validInput);

    var processedData = ProcessData(input);
    Assert.AreEqual(0, processedData.Errors.Count);

    bool writeError = WriteFile(processedData, "temp\file.ext");
    Assert.IsFalse(writeError);

    bool filesAreEqual = CompareFiles("temp\file.ext", "path\to\test\data\case1out.ext");
    Assert.IsTrue(filesAreEqual);
}

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.

Dan Neely
fonte