私は通常、本のアドバイスに従いますレガシータラで効果的に機能する e。依存関係を壊し、コードの一部を@VisibleForTesting public static
メソッドと新しいクラスに移動して、コード(または少なくともその一部)をテスト可能にします。また、新しい関数を変更または追加するときに何も壊さないことを確認するためのテストを作成しています。
同僚は私がこれをするべきではないと言っています。彼の推論:
完全なリファクタリングの時間がない場合、テスト可能な部分を抽出したり、テストを作成したりしないでください。これに私が考慮すべき不利な点はありますか?
これが私の個人的な非科学的な印象です。3つすべての理由は、広くはあるが誤った認知的幻想のように聞こえます。
いくつかの考え:
レガシーコードをリファクタリングしているとき、あなたが書いたテストのいくつかが偶然にも理想的な仕様と矛盾しているかどうかは問題ではありません。 重要なのは、プログラムの現在の動作をテストすることです。リファクタリングは、小さな等機能ステップを実行することですコードをきれいにする;リファクタリング中にバグ修正に関与したくない場合。また、露骨なバグを見つけても失われることはありません。いつでも regression test を記述して一時的に無効にするか、後で使用するためにバックログにバグ修正タスクを挿入できます。一度に一つのことを。
純粋なGUIコードはテストするのが難しく、おそらく「Working Effectively ...」スタイルのリファクタリングには適していないことに同意します。ただし、これは、GUIレイヤーで何もしない動作を抽出して、抽出したコードをテストしてはならないという意味ではありません。そして、「12行、2-3 if/elseブロック」は自明ではありません。少なくとも条件付きロジックを含むすべてのコードをテストする必要があります。
私の経験では、大きなリファクタリングは簡単ではなく、ほとんど機能しません。正確で小さな目標を自分で設定しないと、最後には決して着陸しない、終わりのない、引っ張るようなやり直しに乗り出すリスクが高くなります。変更が大きければ大きいほど、何かを壊すリスクが高まり、どこで失敗したのかを見つけるトラブルが発生します。
アドホックな小さなリファクタリングで物事を次第に改善することは「将来の可能性を損なう」のではなく、それを可能にします-アプリケーションのある湿地を強固にします。あなたは間違いなくそれをすべきです。
また、「元のコードが正しく機能しない可能性があります」-影響を気にせずにコードの動作を変更するだけではありません。他のコードは、壊れたように見えるもの、または現在の実装の副作用に依存している場合があります。既存のアプリケーションのカバレッジをテストすると、後でリファクタリングが簡単になるはずです。何かを誤って壊した場合にそれを見つけるのに役立つからです。最初に最も重要な部分をテストする必要があります。
キリアンの答えは最も重要な側面をカバーしていますが、私はポイント1と3を拡張したいと思います。
開発者がコードを変更(リファクタリング、拡張、デバッグ)したい場合、開発者はそれを理解する必要があります。彼女は自分の変更が彼女が望む動作に正確に影響することを確認する必要があります(リファクタリングの場合は何もない)。
テストがある場合は、テストも理解する必要があります。同時に、テストは彼女がメインコードを理解するのに役立つはずであり、テストはとにかく機能的なコードよりもはるかに簡単に理解できます(悪いテストでない限り)。また、テストは、古いコードの動作の変更点を示すのに役立ちます。元のコードが間違っていて、テストがその間違った動作をテストする場合でも、それは依然として利点です。
ただし、これにはテストが仕様ではなく既存の動作をテストするものとして文書化されている必要があります。
ポイント3についてもいくつか考えます。「大急降下」が実際に発生することはまれであるという事実に加えて、別のこともあります。それは実際には簡単ではないということです。簡単にするために、いくつかの条件を適用する必要があります。
XYZSingleton
という名前ですか?それらのインスタンスゲッターは常にgetInstance()
と呼ばれますか?そして、どのようにして過度に深い階層を見つけますか?神のオブジェクトをどのように検索しますか?これらには、コードメトリック分析が必要で、その後、メトリックを手動で検査します。または、あなたがしたように、あなたは仕事中にそれらにつまずきます。一部の企業には、開発者が追加の価値を直接提供しないコードをいつでも拡張できるようにしたがらない文化があります。新しい機能。
私はおそらくここで改宗者に説教しているのですが、それは明らかに偽りの経済です。簡潔で簡潔なコードは、後続の開発者にメリットをもたらします。回収がすぐに明らかにならないだけです。
私は個人的に (Boy Scout Principle にサブスクライブしますが、他の(あなたが見たように)はサブスクライブしません。
とはいえ、ソフトウェアはエントロピーに悩まされ、技術的負債を増やします。以前の開発者は時間が足りない(または単に怠惰または経験の浅い)場合、適切に設計されたソリューションよりも最適でないバグのあるソリューションを実装している可能性があります。これらをリファクタリングすることは望ましいように思われるかもしれませんが、(とにかくユーザーにとって)機能しているコードに新しいバグをもたらすリスクがあります。
一部の変更は他よりもリスクが低くなります。たとえば、私が作業している場合、影響を最小限に抑えて安全にサブルーチンにファームできる複製コードがたくさんある傾向があります。
最終的には、リファクタリングをどこまで実行するかについて判断を下さなければなりませんが、自動化テストがまだ存在しない場合は、追加することには間違いなく価値があります。
私の経験では、ある種の 特性評価 がうまく機能します。比較的短時間で広範囲の特定のテストカバレッジを提供しますが、GUIアプリケーションに実装するのが難しい場合があります。
次に、変更したいパーツの単体テストを作成し、変更を加えたいときに毎回それを行うことで、長期にわたって単体テストの対象範囲を広げます。
このアプローチは、変更がシステムの他の部分に影響を与えている場合に優れたアイデアを提供し、必要な変更をすぐに行うことができるようになります。
Re:「元のコードが正しく機能しない可能性があります」:
テストは石で書かれていません。それらは変更できます。また、間違った機能をテストした場合、テストをより正確に書き直すのは簡単です。結局のところ、テストされた関数の期待される結果のみが変更されているはずです。
はい、そうです。ソフトウェアテストエンジニアとして回答。まず、とにかくこれまでに行ったことすべてをテストする必要があります。そうしないと、それが機能するかどうかわからないからです。これは当たり前のように思えるかもしれませんが、私には別の見方をする同僚がいます。プロジェクトが決して配信されない小さなプロジェクトであっても、ユーザーを正面から見て、テストしたので機能することを知っていると言う必要があります。
重要なコードには常にバグが含まれており(uniの人を引用しています。バグがない場合は簡単です)、私たちの仕事は、お客様がバグを見つける前にそれらを見つけることです。レガシーコードにはレガシーバグがあります。元のコードが期待どおりに機能しない場合は、それについて知りたいと思います。バグについて知っている場合は問題ありません。バグを見つけることを恐れないでください。それがリリースノートです。
私が正しく覚えているとすれば、リファクタリングの本はとにかく常にテストするように言っているので、それはプロセスの一部です。
自動テストカバレッジを行います。
あなた自身とあなたの顧客と上司の両方による希望的な考えに注意してください。私の変更は初めて正しく、テストは1回だけで済むと信じてみたいのですが、ナイジェリアの詐欺メールを扱うのと同じようにこの種の考え方を扱うことを学びました。まあ、ほとんど;私は詐欺メールに行ったことはありませんが、最近(怒鳴られたとき)、ベストプラクティスを使用しないことに屈しました。それは何度も(高価に)引きずられた苦痛な経験でした。二度と!
Freefallウェブコミックのお気に入りの引用があります。「スーパーバイザーが技術的な詳細について大まかな考えしか持っていない複雑な分野で働いたことはありますか?...そして、スーパーバイザーを失敗させる最も確実な方法は、問題なく彼のあらゆる命令に従いなさい。」
投資する時間を制限することはおそらく適切です。
現在テストされていない大量のレガシーコードを処理している場合は、将来的に大きな書き換えが行われるのを待つのではなく、今すぐテストカバレッジを取得するのが適切です。ユニットテストを書くことから始めるのはそうではありません。
自動テストを行わない場合、コードに変更を加えた後、アプリを手動でエンドツーエンドでテストして、アプリが機能していることを確認する必要があります。それを置き換えるための高レベルの統合テストを書くことから始めます。アプリがファイルを読み込んで検証し、なんらかの方法でデータを処理し、すべてをキャプチャするテストで必要な結果を表示する場合。
理想的には、手動のテスト計画からデータを取得するか、実際の本番データのサンプルを取得して使用できるようにする必要があります。そうでない場合、アプリは製品版であるため、ほとんどの場合、本来あるべきことを行っているので、すべての高いポイントにヒットするデータを作成し、出力が今のところ正しいと仮定します。小さな関数をとることよりも悪くはありません。関数の名前やコメントがそうであると示唆していることを想定し、正しく機能していることを想定してテストを記述します。
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分の統合テストセットを実行するたびに、すでに大きな成功を収めています。