J'ai récemment regardé "Toutes les petites choses" de RailsConf 2014. Au cours de cette conférence, Sandi Metz refactorise une fonction qui comprend une grande instruction if imbriquée:
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
La première étape consiste à diviser la fonction en plusieurs plus petites:
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
Ce que j'ai trouvé intéressant, c'est la façon dont ces petites fonctions ont été écrites. brie_tick
, par exemple, n'a pas été écrit en extrayant les parties pertinentes de la tick
fonction d' origine , mais à partir de zéro en se référant aux test_brie_*
tests unitaires. Une fois tous ces tests unitaires réussis, il a brie_tick
été considéré comme terminé. Une fois toutes les petites fonctions terminées, la tick
fonction monolithique d'origine a été supprimée.
Malheureusement, le présentateur ne semblait pas conscient que cette approche conduisait à trois des quatre *_tick
fonctions étant erronées (et l'autre était vide!). Il existe des cas marginaux dans lesquels le comportement des *_tick
fonctions diffère de celui de la tick
fonction d' origine . Par exemple, @days_remaining <= 0
in brie_tick
devrait être < 0
- donc brie_tick
ne fonctionne pas correctement lorsqu'il est appelé avec days_remaining == 1
et quality < 50
.
Qu'est-ce qui a mal tourné ici? Est-ce un échec des tests - car il n'y avait pas de tests pour ces cas de bord particuliers? Ou un échec de refactoring - parce que le code aurait dû être transformé étape par étape plutôt que réécrit à partir de zéro?
la source
Réponses:
Tous les deux. La refactorisation en utilisant uniquement les étapes standard du livre original de Fowlers est nettement moins sujette aux erreurs que de faire une réécriture, il est donc souvent préférable de n'utiliser que ce type d'étapes pour bébés. Même s'il n'y a pas de tests unitaires pour chaque cas de bord, et même si l'environnement ne fournit pas de refactorings automatiques, un seul changement de code comme «introduire la variable explicative» ou «extraire la fonction» a une chance beaucoup plus petite de changer les détails de comportement du code existant qu'une réécriture complète d'une fonction.
Parfois, cependant, réécrire une section de code est ce dont vous avez besoin ou que vous voulez faire. Et si tel est le cas, vous avez besoin de meilleurs tests.
Notez que même lorsque vous utilisez un outil de refactoring, il y a toujours un certain risque d'introduire des erreurs lorsque vous changez de code, indépendamment de l'application d'étapes plus ou moins importantes. C'est pourquoi le refactoring a toujours besoin de tests. Notez également que les tests ne peuvent que réduire la probabilité de bogues, mais ne prouvent jamais leur absence - néanmoins l'utilisation de techniques telles que l'examen du code et de la couverture de branche peut vous donner un niveau de confiance élevé, et en cas de réécriture d'une section de code, c'est vaut souvent la peine d'appliquer de telles techniques.
la source
brie_tick
tout en ne testant jamais le@days_remaining == 1
cas problématique en testant par exemple avec@days_remaining
set to10
et-10
.L'une des choses qui est vraiment difficile à travailler avec le code hérité: acquérir une compréhension complète du comportement actuel.
Le code hérité sans tests qui contraignent tous les comportements est un modèle courant dans la nature. Ce qui vous laisse une supposition: cela signifie-t-il que les comportements non contraints sont des variables libres? ou des exigences qui ne sont pas spécifiées?
De la conférence :
C'est l'approche la plus conservatrice; si les exigences peuvent être sous-spécifiées, si les tests ne capturent pas toute la logique existante, alors vous devez être très très prudent sur la façon dont vous procédez.
Pour certains, vous pouvez affirmer que si les tests ne décrivent pas correctement le comportement du système, vous avez un "échec de test". Et je pense que c'est juste - mais pas vraiment utile; c'est un problème courant à avoir dans la nature.
Le problème n'est pas tout à fait que les transformations auraient dû être progressives; mais plutôt que le choix de l'outil de refactoring (opérateur de clavier humain? plutôt que l'automatisation guidée) n'était pas bien aligné avec la couverture du test, en raison du taux d'erreur plus élevé.
Cela aurait pu être résolu soit en utilisant des outils de refactoring avec une fiabilité plus élevée, soit en introduisant une batterie de tests plus large pour améliorer les contraintes sur le système.
Je pense donc que votre conjonction est mal choisie;
AND
nonOR
.la source
La refactorisation ne doit pas modifier le comportement visible de l'extérieur de votre code. Voilà l'objectif.
Si vos tests unitaires échouent, cela indique que vous avez modifié le comportement. Mais réussir des tests unitaires n'est jamais l'objectif. Cela aide plus ou moins à atteindre votre objectif. Si le refactoring modifie le comportement visible de l'extérieur et que tous les tests unitaires réussissent, votre refactoring a échoué.
Les tests unitaires de travail dans ce cas ne vous donnent qu'un mauvais sentiment de succès. Mais qu'est-ce qui a mal tourné? Deux choses: le refactoring était imprudent, et les tests unitaires n'étaient pas très bons.
la source
Si vous définissez «correct» comme étant «les tests réussissent», alors par définition, il n'est pas faux de modifier le comportement non testé.
Si un comportement d'arête particulier doit être défini, ajoutez un test pour cela, sinon, alors c'est OK de ne pas se soucier de ce qui se passe. Si vous êtes vraiment pédant, vous pouvez écrire un test qui vérifie
true
dans ce cas de bord pour documenter que vous ne vous souciez pas du comportement.la source