web-dev-qa-db-ja.com

リファクタリング時にユニットテストをどのように機能させますか?

別の質問では、TDDの問題の1つは、リファクタリング中およびリファクタリング後に、テストスイートとコードベースの同期を保つことです。

今、私はリファクタリングの大ファンです。 TDDをやめるつもりはありません。しかし、マイナーなリファクタリングが多くのテスト失敗につながるような方法で書かれたテストの問題も経験しました。

リファクタリング時にテストを壊さないようにするにはどうすればよいですか?

  • テストを「上手に」書いていますか?もしそうなら、あなたは何を探すべきですか?
  • 特定の種類のリファクタリングを避けますか?
  • テストリファクタリングツールはありますか?

編集:私は新しい質問を書きました 私が質問することを尋ねました (しかし、これを興味深い変種として残しました)。

33
Alex Feinman

あなたがやろうとしていることは、実際にはリファクタリングではありません。リファクタリングでは、定義により、変更しないwhatソフトウェアが変更しますhow変更します。

すべてのグリーンテスト(すべて合格)から始め、「内部」で変更を加えます(たとえば、メソッドを派生クラスからベースに移動する、メソッドを抽出する、またはをカプセル化します)CompositeBuilderなど)。テストはまだ成功するはずです。

あなたが説明しているのはリファクタリングではなく、テスト中のソフトウェアの機能を強化する再設計のようです。 TDDとリファクタリング(ここで定義しようとした)は競合していません。 「デルタ」機能を開発するために、引き続きリファクタリング(緑-緑)およびTDD(赤-緑)を適用できます。

38
azheglov

単体テストを使用する利点の1つは、自信を持ってリファクタリングできることです。

リファクタリングでパブリックインターフェイスが変更されない場合は、単体テストをそのままにして、リファクタリング後にすべて通過することを確認します。

リファクタリングによってパブリックインターフェイスが変更される場合は、最初にテストを書き直す必要があります。新しいテストに合格するまでリファクタリングします。

テストを壊してしまうので、リファクタリングは避けません。単体テストを書くことは、お尻の苦痛になる可能性がありますが、長期的には苦痛の価値があります。

21
Tim Murphy

他の回答とは異なり、テストのいくつかの方法canは、テスト中のシステム(SUT)がリファクタリングされると壊れやすくなることに注意することが重要ですifテストはホワイトボックスです。

モックで呼び出されたメソッドのorderを検証するモックフレームワークを使用している場合(呼び出しには副作用がないため、順序が関係ない場合)。次に、これらのメソッド呼び出しが異なる順序でコードがクリーンで、リファクタリングすると、テストが失敗します。一般に、モックはテストに脆弱性をもたらす可能性があります。

プライベートメンバーまたは保護されたメンバーを公開してSUTの内部状態を確認している場合(Visual Basicでは "friend"を使用できます。または、c#でアクセスレベル "internal"をエスカレートして "internalsvisibleto"を使用できます。多くのOO言語、c#a " test-specific-subclass "を使用できます)すると、突然クラスの内部状態が問題になります-クラスをブラックボックスとしてリファクタリングしている可能性があります、しかし、ホワイトボックステストは失敗します。SUTの状態が変化したときに、1つのフィールドが再利用されて別のことを意味するとします(お勧めできません)。これを2つのフィールドに分割すると、壊れたテストを書き直す必要がある場合があります。

テスト固有のサブクラスを使用して保護されたメソッドをテストすることもできます。つまり、プロダクションコードの観点から見たリファクタリングは、テストコードの観点からの重大な変更です。数行をプロテクトメソッドの内外に移動しても、本番環境での副作用はないかもしれませんが、テストは中断されます。

" test hooks "またはその他のテスト固有または条件付きコンパイルコードを使用する場合、内部ロジックへの依存性が壊れやすいため、テストが中断しないようにするのは難しい場合があります。

したがって、テストがSUTの詳細な内部の詳細と結合するのを防ぐために、次のことが役立つ場合があります。

  • 可能であれば、モックではなくスタブを使用します。詳細は Fabio Perieraのトートロジー検定に関するブログトートロジー検定に関する私のブログ を参照してください。
  • モックを使用する場合は、重要でない限り、呼び出されるメソッドの順序を確認しないでください。
  • SUTの内部状態を確認しないようにしてください-可能であれば外部APIを使用してください。
  • 本番コードではテスト固有のロジックを回避するようにしてください
  • テスト固有のサブクラスの使用は避けてください。

上記のすべてのポイントは、テストで使用されるホワイトボックスカップリングの例です。したがって、破壊テストのリファクタリングを完全に回避するには、SUTのブラックボックステストを使用します。

免責事項:ここでのリファクタリングについて説明する目的で、私はWordをもう少し広く使用して、目に見える外部の影響なしに内部の実装を変更することを含めています。一部の純粋主義者は、反対し、アトミックリファクタリング操作について説明しているMartin FowlerとKent Beckの本「リファクタリング」のみを参照している場合があります。

実際には、そこで説明されているアトミック操作よりもわずかに大きいノンブレイクステップを実行する傾向があり、特に、プロダクションコードが外部から同じように動作するようにする変更は、テストに合格しない場合があります。しかし、「同じ動作をする別のアルゴリズムの代替アルゴリズム」をリファクタリングとして含めるのは公平だと私は思います、そしてファウラーは同意すると思います。 Martin Fowler自身は、リファクタリングはテストを壊すかもしれないと言っています:

モックテストを作成するときは、SUTのアウトバウンドコールをテストして、SUTがサプライヤーと適切に通信することを確認します。従来のテストでは、最終的な状態のみが考慮され、その状態がどのように導出されたかは考慮されません。したがって、モックテストは、メソッドの実装により結びついています。共同編集者への呼び出しの性質を変更すると、通常、模擬テストが中断されます。

[...]

実装の変更は従来のテストよりもテストを中断する可能性が高いため、実装への結合はリファクタリングにも干渉します。

ファウラー- モックはスタブではありません

10
perfectionist

リファクタリング中にテストが失敗した場合、定義上、リファクタリングではありません。つまり、「プログラムの動作を変更せずにプログラムの構造を変更する」ということです。

テストの動作を変更する必要がある場合もあります。おそらく、2つのメソッド(たとえば、listen TCPソケットクラス)でbind()とlisten())をマージする必要があるため、コードの他の部分で今すぐ使用しようとして失敗している変更されたAPI。しかし、それはリファクタリングではありません!

5
Frank Shearar

この問題の問題は、「リファクタリング」という言葉の解釈が人によって異なることです。私はあなたがおそらく意味するいくつかのことを注意深く定義することが最善だと思います:

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

他の1人がすでに述べたように、APIを同じに保ち、すべての回帰テストがパブリックAPIで動作する場合、問題はありません。リファクタリングによって問題が発生することはまったくありません。失敗したテストは、古いコードにバグがあり、テストに問題があるか、新しいコードにバグがあることを意味します。

しかし、それはかなり明白です。つまり、リファクタリングとは、APIを変更することを意味します。

だから私はそれに取り組む方法に答えましょう!

  • 最初に、新しいAPIを作成します。これにより、新しいAPIの動作を実現できます。この新しいAPIの名前が古いAPIと同じである場合は、新しいAPIの名前に_NEWという名前を追加します。

    int DoSomethingInterestingAPI();

になる:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

OK-この段階では-DoSomethingInterestingAPI()という名前を使用して、すべての回帰テストが合格です。

次に、コードを調べて、DoSomethingInterestingAPI()へのすべての呼び出しをDoSomethingInterestingAPI_NEW()の適切なバリアントに変更します。これには、新しいAPIを使用するために変更が必要な回帰テストの部分の更新/書き換えが含まれます。

次に、DoSomethingInterestingAPI_OLD()を[[deprecated()]]としてマークします。非推奨のAPIを好きなだけ(APIに依存する可能性のあるすべてのコードを安全に更新するまで)保持してください。

このアプローチを使用すると、回帰テストの失敗は、単にその回帰テストのバグであるか、コード内のバグを特定します-まさに望むとおりです。 APIの_NEWバージョンと_OLDバージョンを明示的に作成することにより、APIを改訂するこの段階的なプロセスにより、しばらくの間、新旧のコードを共存させることができます。

4
Lewis Pringle

リファクタリング中およびリファクタリング後に、テストスイートをコードベースと同期させる

難しいのはカップリングです。すべてのテストには実装の詳細へのある程度のカップリングが付属していますが、ユニットテスト(内部テストに干渉するため、TDDかどうかに関係なく)は特に不利です。ユニットテストが多いほど、ユニットに結合されたコードが多くなります(メソッドシグネチャ/その他のパブリックインターフェイス)ユニットの-少なくとも。

定義による「ユニット」は、低レベルの実装の詳細であり、ユニットのインターフェースは、システムの進化に応じて変更/分割/マージすることができ、変更する必要があります。単体テストが豊富にあると、実際にはこの進化の妨げになる可能性があります。

リファクタリング時にテストを壊さないようにするにはどうすればよいですか?結合を避けてください。実際には、できるだけ多くの単体テストを回避し、実装の詳細にとらわれない、より高いレベルの統合テストを優先することを意味します。特効薬はありませんが、テストは何らかのレベルで何かに結合する必要がありますが、理想的には、セマンティックバージョニングを使用して明示的にバージョン管理されたインターフェイスである必要があります。つまり、通常、公開されたAPI /アプリケーションレベルで行われます(SemVerを実行したくないソリューション内のすべてのユニットについて)。

1
KolA

私はあなたのユニットテストが私が「愚かな」と呼ぶような細かさを持っていると思います:すなわち、それらは各クラスと関数の絶対特徴をテストします。コードジェネレーターツールから離れて、より広い表面に適用するテストを記述したら、アプリケーションのインターフェイスが変更されていなくてもテストを実行できることを確認しながら、内部を必要なだけリファクタリングできます。

すべてのメソッドをテストする単体テストが必要な場合は、それらを同時にリファクタリングする必要があります。

1
gbjbaanb

テストが、要件ではなく実装に密接に結び付いている。

次のようなコメントでテストを書くことを検討してください:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

この方法では、テストから意味をリファクタリングすることはできません。

0
mcintyre321