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


72

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

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

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

完全なリファクタリングの時間がない場合、テスト可能な部分を抽出してテストを書くことを避けるべきですか?これに不利な点はありますか?


29
あなたの同僚は言い訳をしているようです。彼はそのように働いていないからです。人々は時々、このように振る舞います。これは、物事の採用方法を変えるには強情すぎるためです。
Doc Brown 14

3
バグとして分類されるべきものは、コードの他の部分が依存する可能性があります
ラチェットフリーク14

1
私が考えることができるそれに対する唯一の良い議論は、あなたが何かを読み間違えた/誤解した場合、リファクタリング自体が新しいバグを導入する可能性があるということです。そのため、現在開発中のバージョンで自由にリファクタリングおよび修正できますが、過去のバージョンでの修正ははるかに高いハードルに直面し、化粧品/構造のクリーンアップのみの場合は承認されない可能性がありますリスクは潜在的な利益を超えると見なされます。あなたの地元の文化を知ってください-それについての1人の牛飼いの考えだけではなく-他のことをする前に非常に強い理由を用意してください。
ケシュラム14

6
最初のポイントは一種の陽気です-「テストしないでください、バグがあるかもしれません。」それを知っておくのはいいことです。それを修正したいのか、設計仕様が述べているように実際の動作を誰にも変更させたくないのです。どちらにしても、テスト(および自動化されたシステムでテストを実行)は有益です。
クリストファー・クロイツィヒ14

3
あまりにも多くの場合、起こりつつあるすべての病気を治す「大きなリファクタリング」は、神話であり、退屈だと思うもの(テストを書く)を遠い未来に押し付けたいだけの人々によって作られています。そして、それが現実になった場合、彼らはそれが非常に大きくなったことを真剣に後悔するでしょう!
ジュリアヘイワード14

回答:


100

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

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

23
「事実上すべての重大なエラーがコミットされるのは、人々が実際よりも何かをよく理解したと思ったからです」
レム14

ポイント1再-とBDD、テストは自己文書化されている...
ロビーディー

2
@ guillaume31が指摘しているように、テストを書くことの価値の一部は、コードが実際にどのように機能するかを示しています。しかし、それは「間違った」仕様である可能性があります。ビジネスニーズが変更された可能性があり、コードは新しい要件を反映していますが、仕様はそうではありません。単純にコードが「間違っている」と仮定するのは非常に単純です(ポイント1を参照)。繰り返しになりますが、テストでは、コードが実際に何をするのかがわかります。
デビッド14

一度だけでも、コードを理解する必要があります。テストは、リファクタリングせずに書き直しても予期しない動作をキャッチするのに役立ちます(そしてリファクタリングする場合、リファクタリングがレガシー動作を壊さないことを確認するのに役立ちます-またはそれを壊したい場所のみ)。自由に組み込むかどうか-あなたが望むように。
フランクホプキンス

50

いくつかの考え:

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

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

私の経験では、大きなリファクタリングは簡単ではなく、ほとんど機能しません。正確で小さな目標を自分で設定しないと、最後まで足に着地することのない、終わりのない髪を引っ張るリワークに着手するリスクが高くなります。変化が大きければ大きいほど、何かを壊す危険が大きくなり、失敗した場所を見つけるのに苦労することになります。

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


5
「あなたは、プログラムのテスト書き込みテストのための+1 現在の行動を
デビッド・

17

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


悲しいことに本当。クライアントが正確さよりも一貫性を好むため、修正できないエッジケースに現れる明らかなバグがいくつかあります。(これらは原因フィールドブランクの直列に一つのフィールドを残してのようなレポーティングコードは考慮されません事を、許可するデータ収集コードに発生している)
Izkata

14

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

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

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

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

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

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

12

一部の企業には、開発者がいつでも追加機能を直接提供しないコード(たとえば、新機能)を強化することを許可しない文化があります。

私はおそらくここで回心した人に説教しているでしょうが、それは明らかに偽りの経済です。クリーンで簡潔なコードは、その後の開発者に利益をもたらします。それは、回収がすぐに明らかではないということです。

私は個人的にボーイスカウトの原則に加入していますが、他の人(あなたが見たように)はそうではありません。

とはいえ、ソフトウェアはエントロピーに苦しみ、技術的負債を積み上げます。時間の短い以前の開発者(または単に怠け者か経験の浅い開発者)は、適切に設計されたものよりも次善のバグのあるソリューションを実装した可能性があります。これらをリファクタリングすることは望ましいと思われるかもしれませんが、作業中のコード(とにかくユーザーにとって)に新しいバグを導入するリスクがあります。

一部の変更は、他の変更よりもリスクが低くなります。たとえば、私が働いている場所では、影響を最小限に抑えてサブルーチンに安全にファームアウトできる重複コードが大量にある傾向があります。

最終的には、リファクタリングをどの程度行うかについて判断する必要がありますが、自動テストがまだ存在しない場合は、自動テストを追加することには間違いなく価値があります。


2
私は原則として完全に同意しますが、多くの企業では時間とお金が必要です。「片付け」の部分に数分しかかからない場合は問題ありませんが、片付けの見積もりが大きくなり始めたら(大きな定義の場合)、ユーザーコーディングはその決定を上司に委任するか、プロジェクトマネージャ。費やした時間の価値を決めるのはあなたの場所ではありません。バグ修正Xまたは新機能Yの作業は、プロジェクト/会社/顧客にとってはるかに高い価値があるかもしれません。
OZZ

2
また、プロジェクトが6か月後に廃棄されるなどの大きな問題や、会社があなたの時間をより大切にしていること(たとえば、あなたがより重要だと思うことをし、他の誰かがリファクターの仕事をするようになるなど)に気付かないこともあります。リファクタリング作業もテストに影響を与える可能性があります。大規模なリファクタリングは完全なテスト回帰を引き起こしますか?これを行うために展開できるリソースはありますか?
OZZ

はい、あなたが触れたように、主要なコード手術が良いアイデアであるかもしれないし、そうでないかもしれない無数の理由があります:他の開発優先順位、ソフトウェアの寿命、テストリソース、開発者の経験、結合、リリースサイクル、コードに精通しているベース、ドキュメンテーション、ミッション
ロビーディー14

4

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

その後、変更したい部分の単体テストを作成し、変更するたびにそれを行うことで、長期にわたって単体テストの対象範囲を拡大します。

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


3

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

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


1
IMO、少なくともテストしている機能が消滅してなくなるまで、個々のテスト石で書かれるべきです。それらは、既存のシステムの動作を検証するものであり、その動作がすでにその動作に依存している可能性のあるレガシーコードを壊さないことをメンテナーに保証するのに役立ちます。ライブ機能のテストを変更すると、それらの保証が削除されます。
cHao 14

3

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

非自明なコードには常にバグが含まれており(uniからの男を引用します;バグがない場合は自明です)、私たちの仕事は顧客が行う前にそれらを見つけることです。レガシーコードにはレガシーバグがあります。元のコードが期待どおりに機能しない場合は、それについて知りたいと思います。バグについて知っていれば大丈夫です。バグを見つけることを恐れないでください。それがリリースノートの目的です。

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


3

自動化されたテストカバレッジを実行します。

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

フリーフォールのウェブコミックからお気に入りの引用があります。間違いなく彼のすべての命令に従ってください。」

投資する時間を制限するのがおそらく適切です。


1

現在テストされていない大量のレガシーコードを処理している場合、将来の仮想的な大規模な書き換えを待つのではなく、今すぐテストカバレッジを取得するのが正しい方法です。単体テストの作成から始めることはできません。

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

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

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分の統合テストセットを実行するたびに、すでに大きな勝利を収めています。

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