web-dev-qa-db-ja.com

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

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

  1. 従来のクラスのテストを書く
  2. クラス全体をリファクタリングする

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

リファクタリングの前にテストを作成する目的は何ですか?自分のためにより多くの仕事を作成するという学術的な演習に似ています。現在、メソッドのテストを作成しており、物事をテストする方法とレガシーメソッドのしくみについて詳しく学習しています。これは、レガシーコード自体を読み取るだけで学習できますが、テストを書くのは、私の鼻をこすり、この一時的な知識を別のテストで文書化するようなものです。したがって、この方法では、コードが何をしているのかを学ぶしかありません。私はここで一時的なことを言った。なぜなら、コードを一気にリファクタリングするからであり、私の知識は残り、リファクタリングをより新鮮にできることを除いて、すべてのドキュメンテーションとテストはかなりの部分が無効で無効になるからだ。

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

説明してください!

注:

この投稿があります: 完全なリファクタリングの時間がないときに、レガシーコードのテストを書くことは意味がありますか? しかし、「リファクタリング前にテストを書く」と言いますが、「なぜ」とは言いません。 、または「テストの作成」が「すぐに破棄される予定の作業」のように思われる場合の対処方法

15
Dennis

リファクタリングとは、(外部から見える)動作を変更せずに、コードの一部をクリーンアップする(スタイル、デザイン、アルゴリズムを改善するなど)ことです。リファクタリングの前後でコードが同じであることを確認しないようにテストを記述しますis代わりに、リファクタリングの前後でアプリケーションが振る舞うが同じであることを示すインジケータとしてテストを記述します。新しいコードは互換性があり、新しいバグは導入されていません。

あなたの主な関心事はあなたのソフトウェアの公開インターフェースのためのユニットテストを書くことであるべきです。このインターフェースは変更しないでください。したがって、テスト(このインターフェースの自動チェック)も変更しないでください。

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

より大きなスケールで変更を行う場合、これはより困難です。複数の相互依存パーツの再設計。しかし、ある種の境界があり、その境界でテストを書くことができます。

46
amon

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

理想的には、テストはクラスを残りのコードベース、他のシステム、および/またはユーザーインターフェイスとのインターフェイスを通じてのみ処理します。インターフェース。これらの上流または下流のコンポーネントに影響を与えずにインターフェースをリファクタリングすることはできません。それがすべて1つの密結合の混乱である場合、リファクタリングではなくリライトを行う努力を検討することもできますが、それは主にセマンティクスです。

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

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

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

メソッドを3つに分割しても、前のメソッドと同じことを行うため、古いメソッドのテストを実行して3つに分割できます。最初のテストを書く努力は無駄ではありません。

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

7
Philip

私自身の答え/実現:

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

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

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

[〜#〜]更新[〜#〜]

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

テストの書き直しを避けるために実行できる他の解決策があると思います。つまり、古い関数名を新しいコードとその中の綿毛で維持します...しかし、それが最善のアイデアであるかどうかはわかりませんし、何をすべきかを判断するための経験はまだありません。

4
Dennis

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

一般的には、何もないコードにテストを追加します)、コードの動作が期待どおりに機能し続けることを確認するためにリファクタリングするコードです。したがって、リファクタリング中にテストスイートを継続的に実行することは、すばらしいセーフティネットです。テストスイートなしでコードを変更して、変更が予期しないものに影響を与えていないことを確認するという考えは恐ろしいです。

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

3
Michael Durrant

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

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

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

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

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

しかし、振る舞いにsome程度の影響を与えずに、重要なリファクタリングを実際にどのように実行しますか?

以下は、リファクタリング中に発生する可能性があるいくつかの事柄の短いリストです。

  • 変数の名前を変更
  • 名前変更機能
  • 機能を追加
  • 削除機能
  • 関数を2つ以上の関数に分割する
  • 2つ以上の関数を1つの関数に組み合わせる
  • 分割クラス
  • クラスを組み合わせる
  • クラスの名前を変更

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

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

おそらくマクロレベルで動作は変更されませんが、unitテストの要点はマクロの動作を確認することではありません。これはintegrationテストです。単体テストのポイントは、製品を構築する際に使用する個々の要素が破損していないことを確認することです。チェーン、最も弱いリンクなど.

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

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

  • function foo()bar()を呼び出します

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

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

  • すべてが見事に機能します(少なくとも、少なくとも)。

  • あなたはリファクタリングしています...

  • bar()barista()に名前が変更されました

  • flee()barista()を呼び出すように変更されました

  • foo()notを呼び出すように変更barista()

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

そもそもfoo()bar()と呼ばれることに気付かなかったのかもしれません。あなたは確かにflam()bar()によってfoo()に依存していることを理解していませんでした。

なんでも。ポイントは、リファクタリング作業中に、テストがfoo()flam()の両方の新しく壊れた動作を段階的に発見することです。

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

テストがない限り。

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

別のシナリオを考えてみましょう。

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

建物が正しく構築されていることを確認するために、足場を構築します。

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

類推は微妙ですが、重要なのは、製品の構築に役立つツールを構築することは前例のないことではないということです。ツールが永続的でなくても、(必要であっても)便利です。大工は常にジグを作成しますが、1つの仕事だけで作業することもあります。次に、ジグを引き裂き、時にはパーツを使用して、他の仕事のために他のジグを構築します。しかし、それによって治具が役に立たなくなったり、無駄な労力を費やしたりすることはありません。

2
Craig