web-dev-qa-db-ja.com

単体テストでリファクタリングが必要な場合にコミットを構造化する方法

私の仕事での議論から出てきたコミットを構築する方法について、賛否両論の私のリストのレビューを取得しようとしています。

ここにシナリオがあります:

  • 機能Xをレガシーコードベースに追加する必要がある
  • 現在のコードベースにはモックできないものがあり、ユニットテスト機能Xを不可能にしています
  • 単体テストを可能にするためにリファクタリングできますが、機能Xとの共通点がほとんどない他の多くの非テストクラスに影響を与える非常に大きなコード変更が発生します

私の会社には、以下の厳格な規則があります。

  • すべてのコミットはスタンドアロンである必要があります(コンパイル、テストに合格するなど)。これらは合格するまでマージを不可能にする自動化機能を備えています。
  • 早送りマージのみが許可されます(ブランチなし、マージコミットなし、Originリポジトリにはマスターブランチが1つだけあり、完全に直線です)

したがって、問題はこれら3つの事柄に対するコミットをどのように構成するかです。 (リファクタリング、機能X、および機能Xのテスト)同僚が この他の記事 を紹介してくれましたが、リファクタリングの部分には取り組んでいないようです。 (同意するなしリファクタリングソースとテストは1つのコミットで行う必要がある)この記事では、「Gitの二分法の破れ」と「すべてのコミットがコンパイルされることの確認/合格しますが、私たちの厳しいルールはすでにそれをカバーしています。彼らが与える他の主な議論は「論理的に関連するコードが一緒に保たれる」であり、それは私には哲学的に少し思えます。

続行するには3つの方法があります。私はあなたが次のいずれかを行えることを願っています:a)追加b)既存の賛否両論の1つが重要ではなく、リストから削除する必要がある理由についてコメントします。

方法1(1回のコミット):機能X、機能Xのテスト、およびリファクタリングが含まれます

長所:

  • 「論理的に関連するコードが一緒に保たれている」(これが実際に「理由」であるかどうかはわかりません。おそらく3つの方法すべてがこれを行うと主張しますが、そうでない場合もあるかもしれません。しかし、ここでは誰も反対することはできません)。
  • マージの競合なしにチェリーピック/リバートすると、おそらく常にテストをコンパイルしてパスします
  • テストでカバーされていないコードは決してありません

短所:

  • コードレビューが難しい。 (なぜ機能Xに関係なく、このすべてのリファクタリングがここで行われるのですか?)
  • リファクタリングなしでチェリーピックすることはできません。 (リファクタリングをもたらす必要があり、マージの競合の可能性と費やされる時間を増やします)

方法2(2つのコミット):1つは機能Xを含み、2つは機能Xのリファクタリングとテストを含みます

長所:

  • 両方のコードレビューが簡単になりました。 (テストのためにのみ行われたリファクタリングは、関連付けられているテストとともに保持されます)
  • 機能だけをチェリーピックできます。 (例:実験または古いリリースへの機能の追加)
  • 機能を元に戻す場合は、リファクタリングから得られた(うまくいけば)より良い構造化コードを保持できます(ただし、元に戻すことは「純粋」ではありません。以下の短所を参照してください)。

短所:

  • テストカバレッジなしのコミットがあります(それは直後に追加されますが、哲学的に悪いですか?)
  • テストカバレッジなしでコミットすると、すべてのコミットでカバレッジの自動適用が困難/不可能になります(マージするにはy%のカバレッジが必要です)。
  • テストだけをチェリーピックすると、失敗します。
  • 元に戻したい人に負荷をかけます。 (両方のコミットを元に戻すか、機能の一部としてテストを削除して元に戻すことが「純粋」ではないことを知る必要がありました。)

方法3(2つのコミット):1つにはリファクタリングが含まれ、2つには機能Xが含まれ、機能Xのテスト

長所:

  • 2番目のコミットのコードレビューが簡単になりました。 (テストのためにのみ行われたリファクタリングは、機能のコミットには含まれません)
  • マージの競合なしにどちらかを選択する/元に戻す場合、コンパイルしてテストに合格する
  • テストでカバーされていないコードは決してありません(哲学的に優れており、自動カバレッジ実施にとっても簡単です)。

短所:

  • 最初のコミットをコードレビューするのは困難です。 (リファクタリングの唯一の値がテスト用であり、テストが将来のコミットにある場合、2つを行ったり来たりして、なぜそれが行われたのか、そしてそれがよりうまく行われたのかどうかを理解する必要があります。 。]
    • 間違いなく、「論理的に関連するコードが一緒に保たれる」という3つの中で最悪(ただし、それほど重要ではないでしょうか???)

したがって、これらすべてに基づいて、私は3に傾いています。自動化されたテストカバレッジを持つことは大きな勝利です(そして、それが最初にこのうさぎの穴を掘り下げた理由です)。しかし、多分あなたの1人は私が逃した長所/短所を持っていますか?それとも4番目のオプションがありますか?

25
Paul Nogas

既存のコードで作業する場合、機能を実装する前にコードをリファクタリングする必要があるのが一般的です。

これはケントベックのマントラです:「変更を簡単にして(警告:これは難しい場合があります)、簡単に変更してください」

そうするために、私は通常、小さなコミットを頻繁に行うことをお勧めします。赤ちゃんのステップを取ります。段階的にリファクタリングする:

multiple many commits when working on Legacy Code

リファクタリングのたびにコードの動作は変わりませんが、コードの実装方法は変わりません。どちらの実装も同等に有効であるため、「確認するのは難しい」ことではありません。ただし、新しい実装により、変更が容易になります。

最後に、テストを記述して合格にします。それは比較的短く、要点があります。これにより、コミットが読みやすくなります。

そのため、3番目のオプションも選択します。多分私は複数のリファクタリングコミットさえ持っているでしょう。または、レビューのためにプッシュする前にそれらを1つに押しつぶすので、1つだけです。または、リファクタリングのみの最初のPRを実行し、次に機能のみの2番目のPRを実行します。それは本当にどれだけのリファクタリングが必要か(PRを短くしてください)とチームの慣習に依存します!

リファクタリングの唯一の価値がテスト用であり、テストが将来のコミットにある場合、2つを行ったり来たりして、なぜそれが行われたのか、それがより適切に行われたのかどうかを理解する必要があります。

この問題を解決するには、このアプローチでチームを快適にする必要があります。まずリファクタリングしてから、機能を実装します。

同僚と話し合って試してみることをお勧めします。また、「オーバーコミット」を実践して、より小さなコミットを行う習慣を身に付けることをお勧めします。コードがトリッキーなときに役立つスキルなので、コードがそうでないときに実行するのは素晴らしい練習です。

いずれにせよ、同僚との健全な議論はできたと思います。間違いなくあなたのチームに役立つものが見つかります!

34
nicoespeon

3で進みますが、リファクタリングの理由をコミットメッセージに明確に記載してください。その後、誰があなたがそれをしたのかを推測する必要はありません。

多数のファイルに影響するリファクタリングは、少数にしか影響しないリファクタリングよりも常に確認が難しいため、開発サイクルの最初、途中、または最後のいずれで行っても違いはありません。しかし、大規模なリファクタリングandの新機能を1つのコミットに混在させると、レビューが非常に難しくなるため、このルールの方法は1です。

方法2には、新しい機能Xに対してTDDを実行する機会がないという欠点があります。また、単体テストなしで機能Xを追加した後は、後でテストを忘れるリスクがあります。単体テストを追加できるようになる前に最初に大規模なリファクタリングを行うことは、価値があるように見えない可能性があります(これはおそらく誤りですが、これを上司に説明する必要があるかもしれません)。

さらに、私はお勧めします

  • 開始する前に十分なテスト(必ずしも単体テストではない)が用意されていることを確認してください。これにより、リファクタリングによって何も壊されないことが保証されます。そうでない場合は、事前にそのようなテストを追加する時間を取ってください。

  • 機能X +単体テストを完了した後、自分でコードを確認し、事前に行ったリファクタリングが目標に達しているかどうか、およびコードが本当にクリーンな状態にあるかどうかを確認します。そうでない場合は、後で追加のリファクタリング/クリーンアップ手順を追加します。

10
Doc Brown

私は3に投票します。大きな、メンテナンス不可能なレガシーコードの場合、特別なルールに従う必要があります。ほとんどの「常識」ルールは、十分に保守可能な状態にあるコードにのみ適用されます。

3
Euphoric

新しい機能の前にリファクタリングを選択する必要があります(方法番号3)。ただし、新しい機能の前にmultiple timesをリファクタリングすることを検討します。可能であれば、リファクタリングを個別のコミットに分解することをお勧めします。

大きなコミットは避けられない場合がありますが(特に古いコードの場合)、小さいほど良いです。

2
jmoreno

最初に一度にいくつかまたは1つずつサブルーチンを作成し、次に呼び出し元をいくつかまたは1つずつ変更して、内部コードによるサブルーチンの呼び出しとサブルーチンの呼び出しを停止することにより、動作をリファクタリングします。呼び出し元の名前が変更されて比較テストの両方のリビジョンに存在する場合、前後の動作を比較するテストスイートを追加できます。古いコードと比較テストを別のバージョンで削除して、新しいコードの名前をに変更できます。古い名前なので、呼び出し元を変更する必要はありません。

もちろん、優れたテストカバレッジは非常に重要です。すべての奇数ケース、例外、null、ネガティブ、ゼロ除算、NAN、エラー、あらゆる種類のエッジおよびセンターケース。

1

方法3が最も一般的です。

  1. 機能の増分を開発し、関連するテストなしで機能の増分が可能になることはありません。ブランチをプルするときに最初に行うことは、テストを見て、新しく開発されたAPIの使用例を見つけることです。そのコミットはオートコンシステンシーであり、テストを通じて文書化されています

  2. チーム機能に新しい機能を備えた実用的なコードを提供できたら、コードをリファクタリングする準備が整います。リファクタリングは、テストを中断することなく、非常に少ない手順で実行する必要があります。重大な変更は最も安全な方法で行われるべきです

その部分は私が「拡張、置換、削除」と呼んでいるものです

あなたが持っているとしましょう

public void executeBadCode(String what , String why) { ... }

リファクタリングには、パラメーターオブジェクトの導入などの重大な変更が含まれます。

メソッドを追加します

public void excuteBadCode(BadParams b) {
   executeBadCode(b.what() , b.why());
}

単体テストとともに。あなたはそれがリファクタリングであることを指定してコミットします。例えば。 [refact]メソッドexecutebadCode()のパラメーターオブジェクトオーバーロードの導入

ここで、以前の関数のすべての使用法を置き換えます。関連するテストでの潜在的な変更または追加とともに、クラス/ファイルの置換ごとに1つのコミットを1つずつ実行します

最後に、古いメソッドを削除して本体を新しいメソッドに移動し、コードに参照がないことを確認します。

このようにして、すべての開発者はコードの作業バージョンを選択し、それを確認して以前のコードと比較し、行われた1つの意味のある変更を簡単に検出できます。

0
Carmine Ingaldi