web-dev-qa-db-ja.com

完全なリファクタリングの時間がないときに、レガシーコードのテストを書くことは意味がありますか?

私は通常、本のアドバイスに従いますレガシータラで効果的に機能する e。依存関係を壊し、コードの一部を@VisibleForTesting public staticメソッドと新しいクラスに移動して、コード(または少なくともその一部)をテスト可能にします。また、新しい関数を変更または追加するときに何も壊さないことを確認するためのテストを作成しています。

同僚は私がこれをするべきではないと言っています。彼の推論:

  • 元のコードはそもそも正しく機能しない可能性があります。また、開発者もテストを理解して変更する必要があるため、テストを作成すると将来の修正や変更が難しくなります。
  • それが何らかのロジック(たとえば、12行、2〜3個のif/elseブロック)を含むGUIコードである場合、コードはそもそも簡単ではないので、テストは問題に値しません。
  • 同様の悪いパターンがコードベースの他の部分にも存在する可能性があります(まだ見ていませんが、私はかなり新しいです)。 1つの大きなリファクタリングですべてをクリーンアップする方が簡単です。ロジックを抽出すると、この将来の可能性が損なわれる可能性があります。

完全なリファクタリングの時間がない場合、テスト可能な部分を抽出したり、テストを作成したりしないでください。これに私が考慮すべき不利な点はありますか?

74
is4

これが私の個人的な非科学的な印象です。3つすべての理由は、広くはあるが誤った認知的幻想のように聞こえます。

  1. 確かに、既存のコードは間違っている可能性があります。それも正しいかもしれません。アプリケーションは全体として価値があるように見えます(そうでなければ、単にそれを破棄します)ので、より具体的な情報がない場合は、それが主に正しいと想定する必要があります。 「全体的にコードが多く含まれるため、テストを書くと事態が難しくなります」というのは、単純すぎて非常に間違った態度です。
  2. 必ず、リファクタリング、テスト、および改善の努力を、最小限の努力で最大の価値を追加する場所に費やしてください。多くの場合、値をフォーマットするGUIサブルーチンは最優先事項ではありません。しかし、「それは簡単です」も非常に間違った態度であるため、何かをテストしないでください。人々が実際よりも何かをよりよく理解したと思ったので、事実上すべての重大なエラーがコミットされます。
  3. 「私たちは将来、一挙にすべてを行う」といい考えです。通常、大急降下は将来もしっかりととどまるが、現在は何も起こらない。私は、「ゆっくりと着実に勝利する」という信念に固執しています。
100
Kilian Foth

いくつかの考え:

レガシーコードをリファクタリングしているとき、あなたが書いたテストのいくつかが偶然にも理想的な仕様と矛盾しているかどうかは問題ではありません。 重要なのは、プログラムの現在の動作をテストすることですリファクタリングは、小さな等機能ステップを実行することですコードをきれいにする;リファクタリング中にバグ修正に関与したくない場合。また、露骨なバグを見つけても失われることはありません。いつでも regression test を記述して一時的に無効にするか、後で使用するためにバックログにバグ修正タスクを挿入できます。一度に一つのことを。

純粋なGUIコードはテストするのが難しく、おそらく「Working Effectively ...」スタイルのリファクタリングには適していないことに同意します。ただし、これは、GUIレイヤーで何もしない動作を抽出して、抽出したコードをテストしてはならないという意味ではありません。そして、「12行、2-3 if/elseブロック」は自明ではありません。少なくとも条件付きロジックを含むすべてのコードをテストする必要があります。

私の経験では、大きなリファクタリングは簡単ではなく、ほとんど機能しません。正確で小さな目標を自分で設定しないと、最後には決して着陸しない、終わりのない、引っ張るようなやり直しに乗り出すリスクが高くなります。変更が大きければ大きいほど、何かを壊すリスクが高まり、どこで失敗したのかを見つけるトラブルが発生します。

アドホックな小さなリファクタリングで物事を次第に改善することは「将来の可能性を損なう」のではなく、それを可能にします-アプリケーションのある湿地を強固にします。あなたは間違いなくそれをすべきです。

50
guillaume31

また、「元のコードが正しく機能しない可能性があります」-影響を気にせずにコードの動作を変更するだけではありません。他のコードは、壊れたように見えるもの、または現在の実装の副作用に依存している場合があります。既存のアプリケーションのカバレッジをテストすると、後でリファクタリングが簡単になるはずです。何かを誤って壊した場合にそれを見つけるのに役立つからです。最初に最も重要な部分をテストする必要があります。

17
Rory Hunter

キリアンの答えは最も重要な側面をカバーしていますが、私はポイント1と3を拡張したいと思います。

開発者がコードを変更(リファクタリング、拡張、デバッグ)したい場合、開発者はそれを理解する必要があります。彼女は自分の変更が彼女が望む動作に正確に影響することを確認する必要があります(リファクタリングの場合は何もない)。

テストがある場合は、テストも理解する必要があります。同時に、テストは彼女がメインコードを理解するのに役立つはずであり、テストはとにかく機能的なコードよりもはるかに簡単に理解できます(悪いテストでない限り)。また、テストは、古いコードの動作の変更点を示すのに役立ちます。元のコードが間違っていて、テストがその間違った動作をテストする場合でも、それは依然として利点です。

ただし、これにはテストが仕様ではなく既存の動作をテストするものとして文書化されている必要があります。

ポイント3についてもいくつか考えます。「大急降下」が実際に発生することはまれであるという事実に加えて、別のこともあります。それは実際には簡単ではないということです。簡単にするために、いくつかの条件を適用する必要があります。

  • リファクタリングするアンチパターンは簡単に見つかる必要があります。すべてのシングルトンはXYZSingletonという名前ですか?それらのインスタンスゲッターは常にgetInstance()と呼ばれますか?そして、どのようにして過度に深い階層を見つけますか?神のオブジェクトをどのように検索しますか?これらには、コードメトリック分析が必要で、その後、メトリックを手動で検査します。または、あなたがしたように、あなたは仕事中にそれらにつまずきます。
  • リファクタリングは機械的である必要があります。ほとんどの場合、リファクタリングの難しい部分は、既存のコードを十分に理解して、変更方法を理解することです。もう一度シングルトン:シングルトンがなくなった場合、どのようにして必要な情報をユーザーに提供しますか?多くの場合、ローカルコールグラフを理解して、どこから情報を入手できるかを理解することを意味します。さて、何がより簡単か:アプリで10個のシングルトンを検索し、それぞれの使用法を理解し(コードベースの60%を理解する必要がある)、それらを取り除きますか?または、すでに理解しているコードを(現在、そのコードに取り組んでいるため)取得し、そこで使用されているシングルトンを取り除きますか?リファクタリングがあまりにも機械的ではないため、周囲のコードの知識がほとんどまたはまったく必要ない場合は、それを束ねることはできません。
  • リファクタリングは自動化する必要があります。これはやや意見ベースですが、ここに行きます。少しのリファクタリングは楽しくて満足です。多くのリファクタリングは退屈で退屈です。作業したばかりのコードをより良い状態にしておくと、より興味深いものに移る前に、素敵で温かい気持ちになります。コードベース全体をリファクタリングしようとすると、それを書いたばかプログラマーにイライラしたり怒ったりします。大規模なリファクタリングを行いたい場合は、フラストレーションを最小限に抑えるために大幅に自動化する必要があります。これは、ある意味で、最初の2つのポイントの融合です。リファクタリングを自動化できるのは、不良コードの検索を自動化できる(つまり簡単に見つけることができる)場合と、コードの変更を自動化する(つまり機械的な)場合のみです。
  • 徐々に改善することで、ビジネスケースが改善されます。大規模なリファクタリングは非常に破壊的です。コードの一部をリファクタリングすると、変更しているメソッドを5つの部分に分割するだけなので、コードを扱っている他の人とのマージの競合が必ず発生します。適度なサイズのコードをリファクタリングすると、数人のユーザーと競合します(600行のメガファンクションを分割する場合は1〜2、godオブジェクトを分割する場合は2〜4、モジュールからシングルトンを削除する場合は5)。 )、しかし、あなたのメインの編集のためにとにかくそれらの競合があったでしょう。コードベース全体のリファクタリングを行うと、everyoneと競合します。言うまでもなく、それは数日間、数人の開発者を拘束します。段階的に改善すると、各コードの変更に少し時間がかかります。これにより、予測が容易になり、クリーンアップ以外に何も起こらないような目に見える期間はありません。
14
Sebastian Redl

一部の企業には、開発者が追加の価値を直接提供しないコードをいつでも拡張できるようにしたがらない文化があります。新しい機能。

私はおそらくここで改宗者に説教しているのですが、それは明らかに偽りの経済です。簡潔で簡潔なコードは、後続の開発者にメリットをもたらします。回収がすぐに明らかにならないだけです。

私は個人的に Boy Scout Principle にサブスクライブしますが、他の(あなたが見たように)はサブスクライブしません。

とはいえ、ソフトウェアはエントロピーに悩まされ、技術的負債を増やします。以前の開発者は時間が足りない(または単に怠惰または経験の浅い)場合、適切に設計されたソリューションよりも最適でないバグのあるソリューションを実装している可能性があります。これらをリファクタリングすることは望ましいように思われるかもしれませんが、(とにかくユーザーにとって)機能しているコードに新しいバグをもたらすリスクがあります。

一部の変更は他よりもリスクが低くなります。たとえば、私が作業している場合、影響を最小限に抑えて安全にサブルーチンにファームできる複製コードがたくさんある傾向があります。

最終的には、リファクタリングをどこまで実行するかについて判断を下さなければなりませんが、自動化テストがまだ存在しない場合は、追加することには間違いなく価値があります。

12
Robbie Dee

私の経験では、ある種の 特性評価 がうまく機能します。比較的短時間で広範囲の特定のテストカバレッジを提供しますが、GUIアプリケーションに実装するのが難しい場合があります。

次に、変更したいパーツの単体テストを作成し、変更を加えたいときに毎回それを行うことで、長期にわたって単体テストの対象範囲を広げます。

このアプローチは、変更がシステムの他の部分に影響を与えている場合に優れたアイデアを提供し、必要な変更をすぐに行うことができるようになります。

5
jamesj

Re:「元のコードが正しく機能しない可能性があります」:

テストは石で書かれていません。それらは変更できます。また、間違った機能をテストした場合、テストをより正確に書き直すのは簡単です。結局のところ、テストされた関数の期待される結果のみが変更されているはずです。

3
rem

はい、そうです。ソフトウェアテストエンジニアとして回答。まず、とにかくこれまでに行ったことすべてをテストする必要があります。そうしないと、それが機能するかどうかわからないからです。これは当たり前のように思えるかもしれませんが、私には別の見方をする同僚がいます。プロジェクトが決して配信されない小さなプロジェクトであっても、ユーザーを正面から見て、テストしたので機能することを知っていると言う必要があります。

重要なコードには常にバグが含まれており(uniの人を引用しています。バグがない場合は簡単です)、私たちの仕事は、お客様がバグを見つける前にそれらを見つけることです。レガシーコードにはレガシーバグがあります。元のコードが期待どおりに機能しない場合は、それについて知りたいと思います。バグについて知っている場合は問題ありません。バグを見つけることを恐れないでください。それがリリースノートです。

私が正しく覚えているとすれば、リファクタリングの本はとにかく常にテストするように言っているので、それはプロセスの一部です。

3
RedSonja

自動テストカバレッジを行います。

あなた自身とあなたの顧客と上司の両方による希望的な考えに注意してください。私の変更は初めて正しく、テストは1回だけで済むと信じてみたいのですが、ナイジェリアの詐欺メールを扱うのと同じようにこの種の考え方を扱うことを学びました。まあ、ほとんど;私は詐欺メールに行ったことはありませんが、最近(怒鳴られたとき)、ベストプラクティスを使用しないことに屈しました。それは何度も(高価に)引きずられた苦痛な経験でした。二度と!

Freefallウェブコミックのお気に入りの引用があります。「スーパーバイザーが技術的な詳細について大まかな考えしか持っていない複雑な分野で働いたことはありますか?...そして、スーパーバイザーを失敗させる最も確実な方法は、問題なく彼のあらゆる命令に従いなさい。」

投資する時間を制限することはおそらく適切です。

3
Technophile

現在テストされていない大量のレガシーコードを処理している場合は、将来的に大きな書き換えが行われるのを待つのではなく、今すぐテストカバレッジを取得するのが適切です。ユニットテストを書くことから始めるのはそうではありません。

自動テストを行わない場合、コードに変更を加えた後、アプリを手動でエンドツーエンドでテストして、アプリが機能していることを確認する必要があります。それを置き換えるための高レベルの統合テストを書くことから始めます。アプリがファイルを読み込んで検証し、なんらかの方法でデータを処理し、すべてをキャプチャするテストで必要な結果を表示する場合。

理想的には、手動のテスト計画からデータを取得するか、実際の本番データのサンプルを取得して使用できるようにする必要があります。そうでない場合、アプリは製品版であるため、ほとんどの場合、本来あるべきことを行っているので、すべての高いポイントにヒットするデータを作成し、出力が今のところ正しいと仮定します。小さな関数をとることよりも悪くはありません。関数の名前やコメントがそうであると示唆していることを想定し、正しく機能していることを想定してテストを記述します。

IntegrationTestCase1()
{
    var input = ReadDataFile("path\to\test\data\case1in.ext");
    bool validInput = ValidateData(input);
    Assert.IsTrue(validInput);

    var processedData = ProcessData(input);
    Assert.AreEqual(0, processedData.Errors.Count);

    bool writeError = WriteFile(processedData, "temp\file.ext");
    Assert.IsFalse(writeError);

    bool filesAreEqual = CompareFiles("temp\file.ext", "path\to\test\data\case1out.ext");
    Assert.IsTrue(filesAreEqual);
}

アプリの通常の操作と最も一般的なエラーのケースをキャプチャするために記述されたこれらの高レベルのテストを十分に取得したら、キーボード以外の何かを実行しているコードからのエラーを試行してキャッチするためにキーボードを叩く必要がある時間の長さあなたはそれがやるべきことを大幅に下がって将来のリファクタリング(または大幅な書き直し)をはるかに容易にするだろうと思っていました。

単体テストのカバレッジを拡張できるので、統合テストのほとんどを削減または廃止することもできます。アプリのファイルの読み取り/書き込みまたはDBへのアクセスの場合、それらの部分を個別にテストし、それらをモックアウトするか、ファイル/データベースから読み取ったデータ構造を作成することからテストを開始することは、明白な出発点です。実際にそのテストインフラストラクチャを作成することは、一連の迅速でダーティなテストを作成するよりもはるかに時間がかかります。また、統合テストの対象となった部分のほんの一部を手動でテストするのに30分を費やす代わりに、2分の統合テストセットを実行するたびに、すでに大きな成功を収めています。