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


29

別の質問で、TDDの問題の1つは、リファクタリング中およびリファクタリング後にテストスイートをコードベースと同期させることであることが明らかになりました。

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

リファクタリング時にテストが中断しないようにするにはどうすればよいですか?

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

編集:質問の意味を尋ねる新しい質問を書きました(ただし、これは興味深いバリエーションとして残しました)。


7
TDDを使用する場合、リファクタリングの最初のステップは、失敗するテストを作成し、コードをリファクタリングして動作させることだと考えていました。
マットエレン

IDEもテストをリファクタリングする方法を理解できませんか?

@ThorbjørnRavn Andersen、はい、私は私が尋ねることを尋ねた新しい質問を書いた(しかし、これは興味深い変種として保持した。本質的にあなたが言うことであるアジェグロフの答えを参照)
アレックス・ファインマン

この質問にthar情報を追加することを検討しますか?

回答:


35

あなたがやろうとしているのは、実際にはリファクタリングではありません。リファクタリングを使用すると、定義により、あなたは変更されませんどのようなソフトウェアがない、あなたは変更する方法、それはそれをしません。

すべてのグリーンテスト(すべてパス)から始めて、「内部」で変更を行います(たとえば、派生クラスからベースへのメソッドの移動、メソッドの抽出、BuilderでのCompositeのカプセル化など)。テストは引き続きパスするはずです。

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


7
同じコードXが15箇所をコピーしました。各所でカスタマイズ。それを共通のライブラリにし、Xをパラメーター化するか、戦略パターンを使用してこれらの違いを考慮します。Xの単体テストが失敗することを保証します。Xのクライアントは、パブリックインターフェイスがわずかに変更されるため失敗します。再設計またはリファクタリング?私はそれをリファクタリングと呼びますが、いずれにせよそれはあらゆる種類のことを壊します。一番下の行は、すべてがどのように適合するかを正確に知らない限り、リファクタリングできないということです。その後、テストの修正は退屈ですが、最終的には簡単です。
ケビン

3
テストに絶え間ない調整が必要な場合は、おそらくテストが詳細すぎることのヒントです。たとえば、特定の状況で特定の順序でイベントA、B、Cをトリガーする必要があるコードがあるとします。古いコードはABCの順序でそれを行い、テストはその順序でイベントを予期します。リファクタリングされたコードがACBの順序でイベントを吐き出す場合、仕様どおりに動作しますが、テストは失敗します。
オットー

3
@Kevin:パブリックインターフェイスが変更されたため、あなたが説明したのは再設計だと思います。Fowlerのリファクタリングの定義(「外部の振る舞いを変更せずに[コードの]内部構造を変更する」)は、それについて非常に明確です。
アジェグロフ

3
@azheglov:多分が、私の経験では実装が悪いので、インタフェースがされている場合
ケビン・

2
完全に有効で明確な質問は、「言葉の意味」の議論になります。誰があなたがそれを呼ぶかを気にします、どこかでその議論をしましょう。それまでの間、この答えは実際の答えを完全に省略していますが、依然として最も多くの賛成票を持っています。人々がTDDを宗教と呼ぶ理由がわかります。
ダークボーア

21

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

リファクタリングによってパブリックインターフェイスが変更されない場合は、ユニットテストをそのままにして、リファクタリング後にすべてが成功することを確認します。

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

テストを壊してしまうので、リファクタリングを決して避けません。単体テストを書くのは苦痛かもしれませんが、長い目で見れば痛みに値します。


7

他の回答とは反対に、テストがホワイトボックスの場合、テスト対象のシステム(SUT)がリファクタリングされると、テストのいくつかの方法が脆弱になる可能性があることに注意することが重要です。

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

プライベートまたは保護されたメンバーを公開してSUTの内部状態を確認している場合(Visual Basicで「friend」を使用するか、c#でアクセスレベル「internal」をエスカレートし、「internalsvisibleto」を使用できます。 c#「test-specific-subclass」を使用できます)、クラスの内部状態が突然重要になります-クラスをブラックボックスとしてリファクタリングすることはできますが、ホワイトボックステストは失敗します。SUTの状態が変化したときに単一のフィールドが異なることを意味するために再利用されたとしましょう(良い習慣ではありません!)。

テスト固有のサブクラスを使用して、保護されたメソッドをテストすることもできます。これは、生産コードの観点からのリファクタリングがテストコードの観点からの重大な変更であることを意味する場合があります。保護されたメソッドにいくつかの行を出し入れすると、実動の副作用はないかもしれませんが、テストは中断します。

テストフック」またはその他のテスト固有または条件付きコンパイルコードを使用する場合、内部ロジックへの脆弱な依存性のためにテストが中断しないことを保証するのは困難です。

したがって、テストがSUTの親密な内部詳細に結び付くのを防ぐには、次のことが役立ちます。

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

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

免責事項:ここでリファクタリングを議論する目的で、目に見える外部効果なしに内部実装の変更を含めるために、私はもう少し広く言葉を使用しています。一部の純粋主義者は、マーティン・ファウラーとケント・ベックの本「リファクタリング-アトミックリファクタリング操作について説明している」に異議を唱え、排他的に言及する場合があります。

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

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

[...]

実装の変更は、従来のテストよりもテストを壊す可能性がはるかに高いため、実装への結合もリファクタリングの妨げになります。

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


ファウラーは文字通りリファクタリングに関する本を書きました。ユニットテストに関する最も権威のある本(Gerard MeszarosによるxUnit Test Patterns)はFowlerの「署名」シリーズに含まれているので、リファクタリングがテストに違反する可能性があると彼が言うのは正しいでしょう。
完璧主義者

5

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

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


テストでテストされたメソッドの名前を変更しただけだったらどうなるでしょうか?テストで名前を変更しない限り、テストは失敗します。ここでは、彼はプログラムの動作を変更していません。
オスカーメデロス

2
その場合、彼のテストもリファクタリングされています。ただし、注意する必要があります。最初にメソッドの名前を変更してから、テストを実行します。正しい理由で失敗するはずです(コンパイルできない(C#)、MessageNotUnderstood例外を受け取る(Smalltalk)、何も起こらないようです(Objective-Cのnullを食べるパターン))。次に、誤ってバグを導入していないことを確認して、テストを変更します。つまり、「テストが中断した場合」とは、「リファクタリングの完了後にテストが中断した場合」を意味します。変更チャンクを小さくしてみてください!
フランクシェラー

1
単体テストは、本質的にコードの構造に結合されています。例えば、ファウラーはの多く持っているrefactoring.com/catalog(例えば、非表示法、インライン方式の例外を除いて、エラーコードを置き換えるなど)ユニットテストに影響を与えること。
クリスチャンH

偽。2つのメソッドをマージすることは明らかに正式名称を持つリファクタリングであり(たとえば、インラインメソッドリファクタリングは定義に適合します)、インライン化されたメソッドのテストを中断します。単体テストを壊すためにプログラムの動作を変更する必要はありません。単体テストを組み合わせた内部構造を再構築するだけです。プログラムの動作が変わらない限り、これはリファクタリングの定義に適合します。
KolA

よく書かれたテストを想定して上記を書きました:実装をテストしている場合-テストの構造がテスト中のコードの内部を反映している場合、確かに。その場合、実装ではなくユニットの契約をテストします。
フランクシェラー

4

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

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

他の人がすでに指摘したように、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の_NEWおよび_OLDバージョンを明示的に作成することにより、APIを修正するこの段階的なプロセスにより、しばらくの間、新しいコードと古いコードの一部を共存させることができます。


SUTに対する単体テストが公開されたApiの外部クライアントと同じであることを明らかにするため、この回答が気に入っています。処方するものは、「依存性地獄」を回避するために公開されたライブラリ/コンポーネントを管理するSemVerプロトコルに非常に似ています。ただし、これには時間と柔軟性が伴います。このアプローチをすべてのマイクロユニットのパブリックインターフェイスに外挿すると、コストも外挿することになります。より柔軟なアプローチは、可能な限りテストを実装から切り離すことです。つまり、統合テストまたはテストの入力と出力を記述する別のDSL
KolA

1

あなたのユニットテストは、私が「愚かな」と呼ぶ粒度であると思います:)、つまり、それらは各クラスと関数の絶対的な特徴をテストします。コードジェネレーターツールから離れて、より大きなサーフェスに適用するテストを作成すると、アプリケーションのインターフェイスが変更されておらず、テストが機能していることを確認しながら、必要なだけ内部をリファクタリングできます。

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


1
質問に実際に対処する最も有用な答え-内部雑学の不安定な基盤の上にテストカバレッジを構築しないでください。これは、過度に誇張されたアプローチに関する不都合な真実を指摘するために得られるものです。
KolA

1

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

それを難し​​くしているのは結合です。テストには実装の詳細へのある程度の結合が伴いますが、ユニットテスト(TDDであるかどうかに関係なく)は内部に干渉するため、特に不利です。ユニットの-少なくとも。

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

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


0

テストは、要件ではなく実装に密接に結合されています。

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.