Eu assisti recentemente "All the Little Things" do RailsConf 2014. Durante essa palestra, Sandi Metz refatora uma função que inclui uma declaração if aninhada grande:
def tick
if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
if @quality > 0
if @name != 'Sulfuras, Hand of Ragnaros'
@quality -= 1
end
end
else
...
end
...
end
O primeiro passo é dividir a função em várias menores:
def tick
case name
when 'Aged Brie'
return brie_tick
...
end
end
def brie_tick
@days_remaining -= 1
return if quality >= 50
@quality += 1
@quality += 1 if @days_remaining <= 0
end
O que achei interessante foi a maneira como essas funções menores foram escritas. brie_tick
, por exemplo, não foi escrito extraindo as partes relevantes da tick
função original , mas a partir do zero, consultando os test_brie_*
testes de unidade. Depois que todos esses testes de unidade foram aprovados, brie_tick
foi considerado feito. Depois que todas as pequenas funções foram concluídas, a tick
função monolítica original foi excluída.
Infelizmente, o apresentador parecia não ter consciência de que essa abordagem fazia com que três das quatro *_tick
funções estivessem erradas (e a outra estava vazia!). Existem casos extremos nos quais o comportamento das *_tick
funções difere do da tick
função original . Por exemplo, @days_remaining <= 0
em brie_tick
deve ser < 0
- portanto brie_tick
, não funciona corretamente quando chamado com days_remaining == 1
e quality < 50
.
O que deu errado aqui? Isso é uma falha no teste - porque não houve testes para esses casos específicos? Ou uma falha na refatoração - porque o código deveria ter sido transformado passo a passo em vez de reescrito do zero?
fonte
Respostas:
Ambos. A refatoração usando apenas as etapas padrão do livro original de Fowlers é definitivamente menos propensa a erros do que a reescrita, portanto, é preferível usar apenas esses tipos de etapas de bebê. Mesmo que não haja testes de unidade para todos os casos extremos, e mesmo que o ambiente não forneça refatorações automáticas, uma única alteração de código como "introduzir variável de explicação" ou "função de extração" tem uma chance muito menor de alterar os detalhes de comportamento do código existente do que uma reescrita completa de uma função.
Às vezes, no entanto, reescrever uma seção do código é o que você precisa ou deseja fazer. E se for esse o caso, você precisa de melhores testes.
Observe que, mesmo ao usar uma ferramenta de refatoração, sempre existe um certo risco de introdução de erros ao alterar o código, independentemente da aplicação de etapas menores ou maiores. É por isso que a refatoração sempre precisa de testes. Observe também que os testes podem apenas reduzir a probabilidade de erros, mas nunca provar sua ausência - no entanto, o uso de técnicas como olhar o código e a cobertura da ramificação pode fornecer um alto nível de confiança e, no caso de uma reescrita de uma seção de código, é muitas vezes vale a pena aplicar essas técnicas.
fonte
brie_tick
enquanto ainda não estiver testando o@days_remaining == 1
caso problemático , por exemplo, testando com@days_remaining
definido como10
e-10
.Uma das coisas realmente difíceis de trabalhar com o código herdado: adquirir uma compreensão completa do comportamento atual.
Código legado sem testes que restringem todos os comportamentos é um padrão comum na natureza. O que deixa você com um palpite: isso significa que os comportamentos irrestritos são variáveis livres? ou requisitos não especificados?
Da palestra :
Essa é a abordagem mais conservadora; se os requisitos puderem ser subespecificados, se os testes não capturarem toda a lógica existente, você deverá ter muito cuidado com a maneira como proceder.
Por certo, você pode afirmar que, se os testes descrevem inadequadamente o comportamento do sistema, há uma "falha no teste". E acho que é justo - mas não é realmente útil; esse é um problema comum de se ter na natureza.
A questão não é que as transformações devam ter sido passo a passo; mas antes a escolha da ferramenta de refatoração (operador de teclado humano - em vez da automação guiada) não estava bem alinhada com a cobertura do teste, devido à maior taxa de erros.
Isto poderia ter sido dirigida quer usando ferramentas de refatoração com maior fiabilidade ou pela introdução de uma bateria de testes mais amplo para melhorar as limitações do sistema.
Então eu acho que sua conjunção é mal escolhida;
AND
nãoOR
.fonte
A refatoração não deve alterar o comportamento visível externamente do seu código. Esse é o objetivo.
Se seus testes de unidade falharem, isso indica que você mudou o comportamento. Mas passar nos testes de unidade nunca é o objetivo. Ajuda mais ou menos a alcançar seu objetivo. Se a refatoração alterar o comportamento visível externamente e todos os testes de unidade forem aprovados, a refatoração falhará.
Os testes de unidade de trabalho nesse caso apenas dão a você a sensação errada de sucesso. Mas o que deu errado? Duas coisas: a refatoração foi descuidada e os testes de unidade não foram muito bons.
fonte
Se você definir "correto" como "os testes passam", por definição , não é errado alterar o comportamento não testado.
Se um comportamento específico da borda deve ser definido, adicione um teste, caso contrário, não há problema em não se importar com o que acontece. Se você é realmente pedante, pode escrever um teste que verifique
true
quando, nesse caso extremo, para documentar que não se importa com o comportamento.fonte