リファクタリングするコードのテストを書くのはなぜですか?


15

私は巨大なレガシーコードクラスをリファクタリングしています。リファクタリング(私は推測する)はこれを支持します:

  1. レガシークラスのテストを書く
  2. クラスから一体をリファクタリングする

問題:クラスをリファクタリングしたら、ステップ1のテストを変更する必要があります。たとえば、以前は従来のメソッドにあったものが、代わりに別のクラスになる場合があります。1つの方法でしたが、現在はいくつかの方法である場合があります。レガシークラスの全体像が何か新しいものに抹消される可能性があるため、ステップ1で記述するテストはほとんど無効になります。本質的に、ステップ3を追加します。テストを大量に書き換えます。

リファクタリングの前にテストを書く目的は何ですか?それは自分自身のためにより多くの作品を作成する学術的な運動のように聞こえます。現在、このメソッドのテストを書いていますが、物事をテストする方法と、従来のメソッドがどのように機能するかについて詳しく学んでいます。レガシーコード自体を読むだけでこれを学ぶことができますが、テストを書くことは、その中に鼻をこすりつけることと、別のテストでこの一時的な知識を文書化することにほとんど似ています。したがって、この方法では、コードが何をしているのかを学ぶ以外にほとんど選択肢がありません。私はここで一時的なことを言いました。なぜなら、コードを完全にリファクタリングし、ドキュメントとテストのすべてがかなりの部分で無効になるためです。ただし、私の知識は残り、リファクタリングの新鮮さを保つことができます。

それがリファクタリングの前にテストを書く本当の理由ですか?コードをよりよく理解するのに役立ちますか?別の理由があります!

説明してください!

注意:

この投稿があります完全なリファクタリングの時間がないときにレガシーコードのテストを書くことは理にかなっていますか?しかし、「リファクタリングの前にテストを書く」と言いますが、「なぜ」とは言いません。


1
あなたの前提は間違っています。テストは変更しません。新しいテストを作成します。ステップ3は、「現在無効になっているテストをすべて削除します」。
PDR

1
ステップ3は、「新しいテストの作成。無効なテストの削除」を読み取ることができます。私はまだ元の作品を破壊すること
デニス14年

3
いいえ、ステップ2で新しいテストを作成します。そして、ステップ1は破棄されます。しかし、それは時間の無駄でしたか?いいえ、ステップ2で何も壊していないという安心感が得られるので、新しいテストではそうではありません。
pdr 14年

3
@Dennis-状況に関して多くの懸念を共有していますが、ほとんどのリファクタリングの努力は「オリジナルの作品を破壊する」と考えることができますが、破壊しないなら、1万行のスパゲッティコードから離れることはありませんファイル。おそらく単体テストにも同じことが当てはまります。テスト対象のコードと密接に関連しています。コードが進化し、物事が移動および/または削除されると、ユニットテストも進化します。
DXM 14年

「コードを理解する」ことは小さな利点ではありません。理解できないプログラムをどのようにリファクタリングすると思いますか?それは避けられないことであり、徹底的なテストを書くことよりも、プログラムの真の理解を実証するより良い方法です。また、テストを抽象化すればするほど、後でスクラッチする必要が少なくなるため、最初は高レベルのテストに固執する必要があると言わなければなりません。
ニール14

回答:


46

リファクタリングとは、(外部から見える)動作を変更せずに、コードの一部をクリーンアップすることです(たとえば、スタイル、デザイン、アルゴリズムを改善するなど)。あなたは必ず前とリファクタリング後のコードは、ことを確認していないテストを書くである代わりに、あなたは前とリファクタリング後のアプリケーションのことを指標として試験を書き込み、同じ振る舞いに新しいコード互換性があり、新たなバグが導入されなかった。同じことを。

主な関心事は、ソフトウェアのパブリックインターフェイスの単体テストを作成することです。このインターフェイスは変更しないでください。したがって、テスト(このインターフェイスの自動チェック)も変更しないでください。

ただし、テストはエラーを特定するのにも役立ちます。したがって、ソフトウェアのプライベート部分のテストを記述することも意味があります。これらのテストは、リファクタリング中に変更される予定です。実装の詳細(プライベート関数の命名など)を変更する場合は、最初にテストを更新して変更した期待を反映し、次にテストが失敗することを確認し(期待が満たされない)、実際のコードを変更しますそして、すべてのテストが再びパスすることを確認します。パブリックインターフェイスのテストが失敗し始めることはありません。

これは、より大きなスケールで変更を実行する場合、たとえば複数の共依存部分を再設計する場合はより困難です。しかし、ある種の境界があり、その境界でテストを書くことができます。


6
+1。私の心を読んで、私の答えを書きました。重要なポイント:リファクタリング後も同じバグがまだ存在することを示すために、ユニットテストを書く必要があるかもしれません!
david.pfx 14年

質問:関数名の変更の例で、最初にテストを変更して失敗することを確認するのはなぜですか?もちろん、変更すると失敗します。リンカがコードを結び付けるために使用する接続を切断します。おそらく、新しく選択した名前の別の既存のプライベート関数が存在する可能性があることを期待していますか?また、見逃した場合にはそうではないことを確認する必要がありますか?これにより、OCDとの境界線が確実に確保されることがわかりますが、この場合はやり過ぎのように感じられます。あなたの例のテストが失敗しない理由はありますか?
デニス14年

^ cont:一般的な手法として、コードの段階的な健全性チェックを行って、できるだけ早く問題をキャッチするのが良いと思います。毎回手を洗わなくても病気にならないかもしれませんが、習慣として手を洗うだけで、汚染されたものと接触してもしなくても、全体的に健康になります。ここでは、時々不必要に手を洗ったり、コードを不必要にテストしたりすることがありますが、それはあなたとあなたのコードを健康に保つのに役立ちます。それはあなたのポイントでしたか?
デニス

実際、@ Dennisは、無意識のうちに科学的に正しい実験を説明していました。複数のパラメーターを変更したときに、どのパラメーターが結果に実際に影響したのかわかりません。テストはコードであり、すべてのコードにバグがあることを忘れないでください。コードに触れる前にテストを実行しないことで、プログラマーの地獄に行きますか?確かにそうではありません。テストの実行は理想的ですが、それが必要かどうかは専門家の判断です。さらに、テストがコンパイルされない場合はテストが失敗し、リンカを使用した静的言語だけでなく、動的言語にも適用できることに注意してください。
アモン14年

2
リファクタリング中にさまざまなエラーを修正することから、テストがなければ簡単にコードを移動できなかったことに気付きました。テストは、コードを変更することで導入した動作/機能の「差分」を警告します。
デニス14

7

ああ、レガシーシステムを維持します。

理想的には、テストはクラスを、他のコードベース、他のシステム、および/またはユーザーインターフェイスとのインターフェイスを通じてのみ扱います。インターフェース。これらのアップストリームまたはダウンストリームコンポーネントに影響を与えることなく、インターフェイスをリファクタリングすることはできません。すべてが密結合された混乱である場合は、リファクタリングではなく再書き込みの努力を検討することもできますが、それは主にセマンティクスです。

編集: コードの一部が何かを測定し、単純に値を返す関数を持っているとしましょう。唯一のインターフェイスは、関数/メソッド/ whatnotを呼び出して戻り値を受け取ることです。これは疎結合であり、単体テストが簡単です。メインプログラムにバッファを管理するサブコンポーネントがあり、その呼び出しがすべてバッファ自体、一部の制御変数に依存し、コードの別のセクションを介してエラーメッセージをキックバックする場合、それは密接に結合されていると言えます単体テストが難しい。それでも、十分な量のモックオブジェクトとその他を使用してそれを行うことができますが、面倒になります。特にcで。バッファの動作方法をリファクタリングすると、サブコンポーネントが破損します。
編集を終了

安定したインターフェースを介してクラスをテストしている場合、テストはリファクタリングの前後で有効である必要があります。これにより、それを壊さなかったという自信を持って変更を加えることができます。少なくとも、より多くの自信。

また、増分変更を行うこともできます。これが大規模なプロジェクトである場合、それをすべて分解して、新しいシステムを構築し、テストの開発を開始したいとは思わないでしょう。その一部を変更してテストし、変更によってシステムの残りの部分がダウンしないことを確認できます。または、もしそうなら、リリース時に驚かされるよりも、少なくとも巨大な絡み合った混乱が発生するのを見ることができます。

メソッドを3つに分割することもできますが、それらは前のメソッドと同じことを行うため、古いメソッドのテストを行い、3つに分割することができます。最初のテストを書く労力は無駄になりません。

また、レガシーシステムの知識を「一時的な知識」として扱うことはうまくいきません。レガシーシステムに関しては、それが以前どのように行われていたかを知ることが重要です。「なぜ地獄はそれをするのですか?」という昔の質問に非常に役立ちます。


私は理解していると思いますが、インターフェイスで私を失いました。すなわち、私が書いているテストは、テスト対象のメソッドを呼び出した後、特定の変数が適切に入力されているかどうかをチェックします。これらの変数が変更またはリファクタリングされると、テストも同様になります。私が使用している既存のレガシークラスには、sehごとにインターフェイス/ゲッター/セッターがありません。これにより、変数の変更や作業負荷の軽減が行われます。しかし、繰り返しますが、レガシーコードに関しては、インターフェイスが何を意味するのかわかりません。たぶん私はいくつかを作成できますか?しかし、それはリファクタリングになります。
デニス14年

1
ええ、すべてを行う1つのゴッドクラスがある場合、実際にはインターフェイスはありません。しかし、別のクラスを呼び出す場合、最上位クラスは特定の動作をすることを期待しており、単体テストはそれを確認できます。それでも、リファクタリング中にユニットテストを更新する必要がないとは思わないでしょう。
フィリップ14年

4

私自身の答え/実現:

リファクタリング中にさまざまなエラーを修正することから、テストがなければ簡単にコードを移動できなかったことに気付きました。テストは、コードを変更することで導入した動作/機能の「差分」を警告します。

適切なテストを実施している場合、ハイパーに気付く必要はありません。よりリラックスした態度でコードを編集できます。テストが検証と健全性チェックを行います。

また、私のテストはリファクタリングしたときとほぼ同じままであり、破壊されませんでした。実際に、コードを深く掘り下げると、テストにアサーションを追加する機会がいくつかあることに気付きました。

更新

さて、私はテストを大幅に変更しています:/元の関数をリファクタリングしたため(関数を削除し、代わりに新しいクリーナークラスを作成し、以前は関数の内側にあった綿毛を新しいクラスの外側に移動しました)前に実行したテスト対象コードは、異なるクラス名で異なるパラメーターを取り、異なる結果を生成します(綿毛のある元のコードは、テストする結果が多くありました)。したがって、私のテストはこれらの変更を反映する必要があり、基本的にはテストを新しいものに書き換えています。

テストの書き換えを避けるためにできる他の解決策があると思います。すなわち、新しいコードとその中の綿毛で古い関数名を保持します...しかし、それが最良のアイデアであるかどうかはわかりません。


リファクタリングとともにアプリケーションを再設計したように聞こえます。
ジェフ

それはいつリファクタリングされ、いつ再設計されますか?すなわち、リファクタリングするとき、扱いにくい大きなクラスを小さなクラスに分割したり、移動したりすることは困難です。そう、私はその区別を正確に確信していませんが、おそらく私は両方をやっています。
デニス14

3

テストを使用して、コードを実行します。レガシーコードでは、これは変更するコードのテストを記述することを意味します。そうすれば、それらは別個のアーティファクトではありません。テストは、コードが達成する必要があるものに関するものであるべきであり、それがそれを行う方法の内部の内臓に関するものではありません。

通常、リファクタリングするコードのテストを追加せずに、コードの動作が予想どおりに機能し続けることを確認します。したがって、リファクタリング中にテストスイートを継続的に実行することは、素晴らしい安全策です。テストスイートなしでコードを変更して、変更が予期しないものに影響を与えないことを確認するという考えは恐ろしいものです。

古いテストの更新、新しいテストの作成、古いテストの削除などの本質については、現代の専門的なソフトウェア開発のコストの一部として見ているだけです。


最初の段落は、ステップ1を無視してテストを書いていることを主張しているようです。2番目の段落はそれと矛盾するように見えます。
pdr 14年

私の答えを更新しました。
マイケルデュラント14

2

あなたの特定のケースでリファクタリングの目標は何ですか?

私の答えを我慢するために、私たち全員が(ある程度)TDD(テスト駆動開発)を信じていると仮定します。

リファクタリングの目的が既存の動作を変更せずに既存のコードをクリーンアップすることである場合、リファクタリングする前にテストを記述することで、コードの動作を変更していないことを確認し、成功した場合、テストは前後に成功しますリファクタリングします。

  • テストは、新しい作業が実際に機能することを確認するのに役立ちます。

  • テストでは、おそらく元の作業が機能しない場合も明らかになります。

しかし、動作にある程度影響を与えずに、重要なリファクタリングを実際にどのように行うのでしょうか?

リファクタリング中に発生する可能性のあるいくつかのことの短いリストを次に示します。

  • 変数の名前を変更する
  • 名前変更機能
  • 機能を追加
  • 削除機能
  • 関数を2つ以上の関数に分割する
  • 2つ以上の関数を1つの関数に結合する
  • スプリットクラス
  • クラスを結合する
  • クラスの名前を変更する

これらのリストされたアクティビティのすべて、何らかの方法で行動変えると主張します。

そして、リファクタリングが動作を変更した場合でも、テストはまだ何も壊していないことを確認する方法になると主張します。

多分挙動は、マクロレベルでは変化しないが、点単位のテストは、マクロ動作を保証するものではありません。それが統合テストです。単体テストのポイントは、製品を構築する個々の要素が壊れていないことを確認することです。チェーン、最も弱いリンクなど

このシナリオはどうですか:

  • あなたが持っていると仮定 function bar()

  • function foo() に電話をかける bar()

  • function flee() また、関数を呼び出します bar()

  • 多様性のためにflam()、呼び出しますfoo()

  • すべてが見事に動作します(少なくとも、明らかに)。

  • リファクタリング...

  • bar() に改名されます barista()

  • flee() に変更されます barista()

  • foo()されていないコールに変更barista()

明らかに、両方のテストfoo()flam()なりましたが失敗します。

たぶん、あなたは最初の場所でfoo()呼ばれることに気づかなかったbar()。あなたは確かにそれflam()がに依存しbar()ていることに気づかなかったfoo()

なんでも。ポイントは、あなたのテストは、両方の新規壊れた振る舞いを明らかにすることであるfoo()flam()あなたのリファクタリング作業中にインクリメンタルな方法で。

テストは最終的にリファクタリングに役立ちます。

テストがない場合を除きます。

これは少し不自然な例です。bar()breaksを変更する場合foo()foo()最初から複雑すぎて、分解する必要があると主張する人がいます。しかし、プロシージャは理由により他のプロシージャを呼び出すことができ、すべての複雑さを排除することは不可能ですよね?私たちの仕事は管理することです複雑さを合理的にうまくことです。

別のシナリオを検討してください。

あなたは建物を建設しています。

足場を構築して、建物が正しく構築されるようにします。

足場は、特にエレベーターシャフトの構築に役立ちます。その後、足場を分解しますが、エレベーターのシャフトは残ります。足場を破壊することにより、「オリジナルの作品」を破壊しました。

類推は微々たるものですが、ポイントは、製品を構築するのに役立つツールを構築することは珍しいことではないということです。ツールが永続的ではない場合でも、有用です(必要な場合でも)。大工は常にジグを作成しますが、たった1つの仕事のためだけです。その後、彼らはジグを引き裂き、時には部品を使用して他の仕事のために他のジグを構築しますが、そうでない場合もあります。しかし、それはジグを役に立たない、または無駄な努力をしません。

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