web-dev-qa-db-ja.com

リファクタリング-すべてのテストに合格する限り、単純にコードを書き直すことは適切ですか?

最近、RailsConf 2014の "All the Little Things" を見ました。この講演中に、Sandi Metzは、ネストされた大きなifステートメントを含む関数をリファクタリングします。

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

最初のステップは、関数をいくつかの小さなものに分割することです:

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

私が興味深いと思ったのは、これらの小さな関数の記述方法でした。たとえば、brie_tickは、元のtick関数の関連部分を抽出することによって作成されたのではなく、test_brie_*単体テストを参照してゼロから作成されました。これらのすべての単体テストに合格すると、brie_tickは完了したと見なされました。小さな関数がすべて実行されると、元のモノリシックtick関数が削除されました。

残念ながら、プレゼンターは、このアプローチが4つの*_tick関数の3つが間違っている(そして他の関数が空である!)ことにつながることを知らなかったようです。 *_tick関数の動作が元のtick関数の動作と異なるEdgeケースがあります。たとえば、@days_remaining <= 0brie_tick< 0にする必要があります。したがって、brie_tickは、days_remaining == 1およびquality < 50で呼び出された場合、正しく機能しません。

ここで何が問題になっていますか?これはテストの失敗ですか?これらの特定のEdgeケースのテストがなかったためですか?または、リファクタリングの失敗-コードを最初から書き直すのではなく、段階的に変換する必要があったためですか?

9
user200783

これはテストの失敗ですか?これらの特定のEdgeケースのテストがなかったためですか?または、リファクタリングの失敗-コードを最初から書き直すのではなく、段階的に変換する必要があったためですか?

両方とも。 Fowlersオリジナルブック の標準的なステップのみを使用したリファクタリングは、書き換えを行うよりも間違いなくエラーが発生しにくいため、多くの場合、これらの種類のベビーステップのみを使用することをお勧めします。すべてのEdgeケースの単体テストがなく、環境が自動リファクタリングを提供していない場合でも、「説明変数の導入」や「関数の抽出」などの単一のコード変更では、動作の詳細を変更する機会がはるかに少なくなります関数の完全な書き換えよりも既存のコード。

ただし、場合によっては、コードのセクションを書き換えることが、必要またはやりたいことです。その場合は、より良いテストが必要です。

リファクタリングツールを使用する場合でも、小さいステップまたは大きいステップを適用するかどうかに関係なく、コードを変更すると常にエラーが発生する特定のリスクがあることに注意してください。そのため、リファクタリングには常にテストが必要です。テストはバグの可能性を減らすだけで、バグがないことを証明できないことにも注意してください。それでも、コードやブランチカバレッジを調べるなどの手法を使用すると、高い信頼レベルが得られます。コードセクションを書き換えた場合、多くの場合、このような手法を適用する価値があります。

11
Doc Brown

ここで何が問題になっていますか?これはテストの失敗ですか?これらの特定のEdgeケースのテストがなかったためですか?または、リファクタリングの失敗-コードを最初から書き直すのではなく、段階的に変換する必要があったためですか?

レガシーコードの操作で非常に難しいことの1つは、現在の動作を完全に理解することです。

すべての動作を制約するテストのないレガシーコードは、実際にはcommonパターンです。どちらが当てはまるかというと、制約のない動作が自由変数であることを意味しますか?または十分に指定されていない要件?

講演より

これは、リファクタリングの定義による実際のリファクタリングです。このコードをリファクタリングします。動作を変更せずに配置を変更します。

これはより保守的なアプローチです。要件が十分に指定されていない可能性がある場合、テストで既存のロジックのすべてがキャプチャされない場合は、続行する方法について非常に注意する必要があります。

確かに、テストがシステムの動作を適切に説明していない場合、「テストの失敗」があると断言できます。そして、私はそれは公平だと思いますが、実際には有用ではありません。これは一般的な問題です。

または、リファクタリングの失敗-コードを最初から書き直すのではなく、段階的に変換する必要があったためですか?

問題はquiteではありません。変換は段階的に行われるべきでした。むしろ、エラー率が高いため、リファクタリングツール(ガイド付きオートメーションではなく人間のキーボードオペレーター?)の選択がテストカバレッジとうまく一致していなかったのです。

これには対処できた可能性がありますどちらか信頼性の高いリファクタリングツールを使用するか、より幅広いテストバッテリーを導入してシステムの制約を改善することによって。

だから私はあなたの接続詞がよく選ばれていないと思います。 ANDではなくOR

2
VoiceOfUnreason

リファクタリングによって、外部から見えるコードの動作が変更されることはありません。それが目標です。

単体テストが失敗した場合は、動作が変更されたことを示しています。しかし、単体テストに合格することは決して目標ではありません。それは多かれ少なかれあなたの目標を達成するのに役立ちます。リファクタリングによって外部から見える動作が変更され、すべての単体テストに合格した場合、リファクタリングは失敗しました。

この場合の単体テストは、間違った成功感を与えるだけです。しかし、何が問題でしたか? 2つのこと:リファクタリングは不注意であり、単体テストはあまり良くありませんでした。

2
gnasher729

「正しい」を「テストに合格」と定義した場合、定義により、テストされていない動作を変更することは間違いありません。

特定のEdgeの動作すべきであるが定義されている場合は、そのテストを追加します。定義されていない場合は、何が発生しても問題ありません。本当に知識が豊富な場合は、そのEdgeケースでtrueをチェックするテストを記述して、気にしないドキュメント動作は何ですか。

1
Caleth