リファクタリングする前に単体テストを書く方法は?


55

「リファクタリング時にユニットテストをどのように機能させ続けるのですか?」などの同様の行に沿って質問に対するいくつかの回答を読みました。私の場合、私たちが持っているいくつかの標準をレビューし、それに合わせるプロジェクトを与えられたという点で、シナリオはわずかに異なります。現在、プロジェクトのテストはまったくありません!

サービスレイヤーにDAOタイプのコードを混在させないなど、改善できたと思われる多くのことを特定しました。

リファクタリングする前に、既存のコードのテストを書くことは良い考えのように思えました。私に見える問題は、リファクタリングを行うと、特定のロジックが実行される場所を変更しているため、これらのテストが壊れ、テストが以前の構造を念頭に置いて書かれていることです(依存関係のモックなど)

私の場合、手続きを進める最善の方法は何でしょうか?私はリファクタリングされたコードの周りにテストを書きたいと思っていますが、望みの振る舞いを変える可能性のあるものを間違ってリファクタリングするリスクがあることを知っています。

これがリファクタリングであろうとリデザインであろうと、これらの用語の修正を理解できてうれしいです。現在、次のリファクタリングの定義に取り組んでいます。「リファクタリングでは、定義により、ソフトウェアの動作は変更されません方法を変えることができます。」だから、私はソフトウェアが何をするかを変えていません。

同様に、再設計と考えられるメソッドのシグネチャを変更する場合の議論を見ることができます。

簡単な例を示します

MyDocumentService.java (現在)

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      DataResultSet rs = documentDAO.findAllDocuments();
      List<Document> documents = new ArrayList<>();
      for(DataObject do: rs.getRows()) {
         //get row data create new document add it to 
         //documents list
      }

      return documents;
   }
}

MyDocumentService.java (何でもリファクタリング/再設計)

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      //Code dealing with DataResultSet moved back up to DAO
      //DAO now returns a List<Document> instead of a DataResultSet
      return documentDAO.findAllDocuments();
   }
}

14
あなたが本当に計画しているリファクタリング、または再設計ですか?なぜなら、2つのケースでは答えが異なる可能性があるからです。
ハービー

4
私は定義に取り組んでいます。「リファクタリングでは、定義により、ソフトウェアの動作を変更せず、その方法を変更します。」だから私は、用語の私の理解を修正すること自由に感じ、それがリファクタリングされ、この場合には信じる
PDStat

21
統合テストを作成しないでください。計画している「リファクタリング」は、単体テストのレベルを超えています。新しいクラス(またはそれらを保持していることがわかっている古いクラス)のみを単体テストします。
ハーミングモニカを停止

2
リファクタリングの定義に関して、ソフトウェアはそれが何をするのかを明確に定義していますか?言い換えれば、独立したAPIを備えたモジュールに既に「ファクタリング」されていますか?そうでない場合は、おそらく最高(ユーザー向け)レベルを除いて、リファクタリングできません。モジュールレベルでは、必然的に再設計されます。その場合、ユニットを取得する前にユニットテストを書く時間を無駄にしないでください。
ケビンクルムウィーデ

4
テストハーネスに入れるためだけに、テストのセーフティネットなしで少しリファクタリングしなければならない可能性が非常に高いでしょう。私があなたに与えることができる最善のアドバイスは、あなたのIDEまたはリファクタリングツールがあなたのためにそれをしないなら、手でそれをしないことです。CUTをハーネスにするまで、自動リファクタリングを適用し続けます。あなたは間違いなくマイケル・フェザーの「レガシーコードで効果的に働く」のコピーを手に入れたいと思うでしょう。
ラバーダック

回答:


56

回帰をチェックするテストを探しています。すなわち、いくつかの既存の動作を壊します。まず、その振る舞いが同じレベルであり、その振る舞いを駆動するインターフェースが同じレベルであることを特定し、その時点でテストを開始します。

これで、このレベルより下で何をしても、振る舞いが同じであると断言するいくつかのテストができました。

テストとコードがどのように同期し続けることができるか疑問に思うのは正解です。あなたの場合はインターフェイスコンポーネントには同じまま(あなたが新しい実装を作成すると)、あなたはこの周りのテストを書いて、両方の実装のために同じ条件を主張することができます。そうでない場合は、冗長コンポーネントのテストが冗長テストであることを受け入れる必要があります。


1
Viz、ユニットテストではなく、統合テストまたはシステムテストを行う可能性があります。おそらくまだ「単体テスト」ツールを使用しますが、各テストで複数のコードユニットにヒットすることになります。
モー

はい。それは非常に事実です。あなたのリグレッションテストはうまくやって何かすることができ非常にサーバーへの高レベルなどのREST要求およびおそらく後続のデータベーステスト(すなわち間違いないユニット!テスト)
ブライアンアグニュー

40

推奨されるプラクティスは、おそらくバグを含むコードの現在の動作をテストする「ピンダウンテスト」の作成から始めることですが、要件ドキュメントに違反する特定の動作がバグであるかどうかを見極めるという狂気に陥る必要はありません。知らない何かの回避策、または文書化されていない要件の変更を表します。

これらのピンダウンテストが高レベル、つまり単体テストではなく統合であることが最も理にかなっているため、リファクタリングを開始しても機能し続けます。

ただし、コードをテスト可能にするには、いくつかのリファクタリングが必要になる場合があります。「安全な」リファクタリングに固執するように注意してください。たとえば、ほとんどすべての場合、プライベートなメソッドは何も壊さずに公開できます。


統合テストの場合は+1。アプリによっては、実際にWebアプリにリクエストを送信するレベルで開始できる場合があります。アプリが送り返すものは、リファクタリングのためだけに変更されるべきではありませんが、HTMLを送り返す場合、これは確かにテスト可能ではありません。
jpmc26

「ピンダウン」テストというフレーズが好きです。
ブライアンアグニュー

12

私は、あなたがまだ読んでいないなら、レガシーコードで効果的に働くこととリファクタリングの両方を読むことを提案します- 既存のコードのデザインを改善します

[..]問題は、リファクタリングを行うと、特定のロジックが実行される場所を変更しているときにテストが中断し、以前の構造を念頭に置いてテストが作成されることです(依存関係のモックなど)[ ..]

私はこれを必ずしも問題とは考えていません。テストを書き、コードの構造を変更してから、テスト構造調整します。これにより、新しい構造が実際に古い構造よりも優れているかどうかを直接フィードバックできます。調整されたテストの簡単に記述できるためです(したがって、テストの変更は比較的簡単で、新しく導入されるリスクが低くなるためです)バグはテストに合格します)。

また、他の人がすでに書いているように:あまりにも詳細なテストを書かないでください(少なくとも最初は)。高レベルの抽象化を維持するようにしてください(したがって、テストはおそらく回帰テストまたは統合テストとして特徴付けられるでしょう)。


1
この。テストはひどいように見えますが、既存の動作をカバーします。次に、コードがリファクタリングされると、ロックステップでテストもリファクタリングされます。誇りに思うものが見つかるまで繰り返します。++
ラバーダック

1
私はこれらの本の推奨事項の両方を支持します-テストレスコードを処理しなければならないとき、私は常に手元にあります。
トビースパイト

5

すべての依存関係を模擬する厳密な単体テストを作成しないでください。一部の人々は、これらは実際の単体テストではないと言うでしょう。それらを無視します。これらのテストは有用であり、それが重要なことです。

あなたの例を見てみましょう:

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      DataResultSet rs = documentDAO.findAllDocuments();
      List<Document> documents = new ArrayList<>();
      for(DataObject do: rs.getRows()) {
         //get row data create new document add it to 
         //documents list
      }

      return documents;
   }
}

テストはおそらく次のようになります。

DocumentDao documentDao = Mock.create(DocumentDao.class);
Mock.when(documentDao.findAllDocuments())
    .thenReturn(DataResultSet.create(...))
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

DocumentDaoをモックする代わりに、その依存関係をモックします。

DocumentDao documentDao = new DocumentDao(db);
Mock.when(db...)
    .thenReturn(...)
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

これで、テストを中断MyDocumentServiceするDocumentDaoことなく、ロジックを移行できます。テストでは、機能が同じであることが示されます(テストした限り)。


DocumentServiceをテストしていて、DAOをモックしていない場合は、単体テストではありません。これは、単一テストと統合テストの中間にあります。そうではありませんか?
ライヴ

7
@Laiv、実際、ユニットテストという用語の使用方法にはかなりの多様性があります。厳密に隔離されたテストのみを意味するために使用する人もいます。その他には、迅速に実行されるテストが含まれます。一部には、テストフレームワークで実行されるすべてのものが含まれます。ただし、最終的には、単体テストという用語をどのように定義するかは重要ではありません。問題は、どのテストが有用かということです。そのため、ユニットテストをどのように正確に定義するかについて気を散らさないでください。
ウィンストンイーバート

有用性が最も重要であることを示す優れた点。時間と貴重なリソースの膨大な浪費だけでなく、単体テストが良いことよりも害を及ぼすためだけに、最も些細なアルゴリズムのための贅沢な単体テストは。これはほぼすべてに適用でき、私のキャリアの早い段階で知りたいと思っています。
リー

3

あなたが言うように、振る舞いを変更すると、それは変換であり、リファクタリングではありません。どのレベルで動作を変更するかが違いを生みます。

最高レベルで正式なテストがない場合は、コードが機能すると見なされるように再設計した後もクライアント(コードまたは人間を呼び出す)が同じである必要がある一連の要件を見つけてみてください。これは、実装する必要があるテストケースのリストです。

テストケースの変更を必要とする実装の変更に関する質問に対処するには、Detroit(classical)vs London(mockist)TDDをご覧になることをお勧めします。マーティン・ファウラーは彼のすばらしい記事でこれについて話しています。モックはスタブではありませんが、多くの人が意見を持っています。外観を変更できない最高レベルから始めて、作業を進めていくと、本当に変更する必要があるレベルに達するまで、要件はかなり安定したままになります。

テストなしでは、これは難しくなります。新しいコードが必要なことを正確に実行できることを確認できるまで、クライアントをデュアルコードパスで実行すること(および差分を記録すること)を検討することをお勧めします。


3

ここで私のアプローチ。4段階のリファクタリングテストであるため、時間の面でコストがかかります。

私が公開しようとしているものは、質問の例で公開されているものよりも複雑なコンポーネントの方が適している可能性があります。

とにかく、この戦略は、インターフェイス(DAO、サービス、コントローラーなど)によって正規化されるコンポーネント候補に対して有効です。

1. インターフェース

MyDocumentServiceからすべてのパブリックメソッドを収集し、それらをすべてインターフェイスにまとめることができます。例えば。既に存在する場合は、新しいものを設定する代わりに、その1つを使用します。

public interface DocumentService {

   List<Document> getAllDocuments();

   //more methods here...
}

次に、MyDocumentServiceにこの新しいインターフェイスを実装させます。

ここまでは順調ですね。大きな変更は行われず、現在の契約を尊重し、behaivosは変更されていません。

public class MyDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //legacy code here as it is.
        // with no changes ...
  }
}

2. レガシーコードの単体テスト

ここに大変な仕事があります。テストスイートをセットアップします。できるだけ多くのケースを設定する必要があります。成功したケースとエラーのケースです。これらは最後に結果の品質のためです。

次に、MyDocumentServiceをテストする代わりに、テストするコントラクトとしてインターフェイスを使用します。

私は詳細には立ち入らないので、許してくれ

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

    //... More mocks

   DocumentService service;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
      //this is purposed way to inject 
      //dependencies. Replace it with one you like more.  
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> result = service.getAllDocuments();

          Assert.assertX(result);
          Assert.assertY(result);
           //... As many you think appropiate
    } 
 }

この段階では、このアプローチの他のどの段階よりも時間がかかります。そして、それは将来の比較のための参照点を設定するため、最も重要です。

注:大きな変更は行われなかったため、動作は変更されていません。ここでSCMにタグを付けることをお勧めします。タグやブランチは関係ありません。バージョンを実行するだけです。

ロールバック、バージョン比較、および古いコードと新しいコードの並列実行用に必要な場合があります。

3. リファクタリング

リファクタリングは新しいコンポーネントに実装されます。既存のコードに変更を加えることはありません。最初のステップは、コピー&ペーストを行うためにするのと同じくらい簡単ですMyDocumentServiceとに名前を変更CustomDocumentService(例えば)。

新しいクラスはDocumentServiceを実装し続けます。次に、getAllDocuments()をリファクタリングします。(1から始めます。ピンリファクタリング)

DAOのインターフェース/メソッドの変更が必要になる場合があります。その場合、既存のコードを変更しないでください。DAOインターフェイスに独自のメソッドを実装します。古いコードにDeprecatedの注釈を付ければ、後で何を削除すべきかがわかります。

既存の実装を壊したり変更したりしないことが重要です。両方のサービスを並行して実行し、結果を比較する必要があります。

public class CustomDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //new code here ...
         //due to im refactoring service 
         //I do the less changes possible on its dependencies (DAO).
         //these changes will come later 
         //and they will have their own tests
  }
 }

4. DocumentServiceTestSuiteの更新

さて、今は簡単な部分です。新しいコンポーネントのテストを追加します。

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

   DocumentService service;
   DocumentService customService;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
        customService = CustomDocumentService(mockDepA, mockDepB);
       // this is purposed way to inject 
       //dependencies. Replace it with the one you like more
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> oldResult = service.getAllDocuments();

          Assert.assertX(oldResult);
          Assert.assertY(oldResult);
           //... As many you think appropiate

          List<Document> newResult = customService.getAllDocuments();

          Assert.assertX(newResult);
          Assert.assertY(newResult);
           //... The very same made to oldResult

          //this is optional
Assert.assertEquals(oldResult,newResult);
    } 
 }

これで、oldResultとnewResultの両方が独立して検証されましたが、相互に比較することもできます。この最後の検証はオプションであり、結果に依存します。比較できないかもしれません。

この方法で2つのコレクションを比較するにはあまり見かけませんが、他の種類のオブジェクト(pojo、データモデルエンティティ、DTO、ラッパー、ネイティブタイプなど)には有効です。

ノート

単体テストの実行方法やモックライブラリーの使用方法を敢えて教えません。リファクタリングをどのように行わなければならないかを言うつもりもありません。私がやりたかったのは、グローバル戦略を提案することです。それをどう進めるかはあなた次第です。コードの仕組み、複雑さ、およびそのような戦略が試してみる価値があるかどうかを正確に知っています。ここでは、時間やリソースなどの事実が重要です。また、今後これらのテストから何を期待するかも重要です。

私はサービスによってサンプルを開始しましたが、DAOなどをフォローします。依存関係レベルに深く入ります。多かれ少なかれ、それはアップボトム戦略として説明できます。ただし、マイナーな変更/リファクタリング(ツアーの例で公開されているものなど)の場合、ボトムアップでタスクが簡単になります。変更の範囲が少ないためです。

最後に、廃止されたコードを削除し、古い依存関係を新しい依存関係にリダイレクトするのはあなた次第です。

廃止されたテストも削除し、ジョブは完了しました。テストで古いソリューションをバージョン管理した場合は、いつでも相互に確認および比較できます。

非常に多くの作業の結果、レガシーコードのテスト、検証、およびバージョン管理が行われます。そして、テスト、検証、およびバージョン化の準備ができた新しいコード。


3

tl; dr単体テストを作成しません。より適切なレベルでテストを作成します。


リファクタリングの実際の定義を考えると:

ソフトウェアの動作を変更せず、ソフトウェアの動作を変更します

そこにある非常に広いスペクトルが。一端は、おそらくより効率的なアルゴリズムを使用して、特定のメソッドに自己完結型の変更を加えることです。もう一方は、別の言語に移植しています。

どのレベルのリファクタリング/再設計が実行されようとも、そのレベル以上で動作するテストを持つことが重要です。

自動テストは多くの場合、レベルごとに次のように分類されます。

  • 単体テスト -個々のコンポーネント(クラス、メソッド)

  • 統合テスト -コンポーネント間の相互作用

  • システムテスト -完全なアプリケーション

基本的に手つかずのリファクタリングに耐えられるテストのレベルを記述します。

考える:

アプリケーションは、両方の本質的な、公的に目に見える行動を持つことになります後にリファクタリング?それでも同じことをテストするにはどうすればよいですか?


2

インターフェイスが重要な方法で変更されることが予想されるポイントでフックするテストを書く時間を無駄にしないでください。これは、多くの場合、本質的に「協調的」なクラスを単体テストしようとしていることの兆候です-その価値は、それ自体が行うことではなく、価値のある行動を生み出すために多くの密接に関連するクラスと相互作用する方法にあります。それはテストするその動作です。つまり、より高いレベルでテストしたいということです。このレベル以下のテストでは、多くのlotいm笑が必要になることが多く、結果として生じるテストは、行動を守るための支援というよりも、開発上の抵抗になる可能性があります。

リファクタリング、リデザイン、その他何をしているのかを考えすぎないでください。下位レベルで多くのコンポーネントの再設計を構成する変更を加えることができますが、より高い統合レベルでは単にリファクタリングになります。重要なのは、どの行動があなたにとって価値があるのか​​を明確にし、あなたが行くときにその行動を守ることです。

テストを作成する際に考慮すると便利です。このテストが実際にテストしていることをQA、製品所有者、またはユーザーに簡単に説明できますか?テストを説明するのが難解で技術的すぎると思われる場合は、間違ったレベルでテストしている可能性があります。「理にかなっている」ポイント/レベルでテストし、すべてのレベルのテストでコードを使いすぎないようにします。


ダウン投票の理由に常に興味があります!
トポモルト

1

最初のタスクは、テスト用の「理想的なメソッドシグネチャ」を考え出すことです。それを純粋な機能にしようと努力します。これは、実際にテスト中のコードから独立している必要があります。それは小さなアダプター層です。このアダプターレイヤーにコードを記述します。これで、コードをリファクタリングするときに、アダプターレイヤーを変更するだけで済みます。以下に簡単な例を示します。

[TestMethod]
public void simple_addition()
{
    Assert.AreEqual(7, Eval("3 + 4"));
}

[TestMethod]
public void order_of_operations()
{
    Assert.AreEqual(52, Eval("2 + 5 * 10"));
}

[TestMethod]
public void absolute_value()
{
    Assert.AreEqual(9, Eval("abs(-9)"));
    Assert.AreEqual(5, Eval("abs(5)"));
    Assert.AreEqual(0, Eval("abs(0)"));
}

static object Eval(string expression)
{
    // This is the code under test.
    // I can refactor this as much as I want without changing the tests.
    var settings = new EvaluatorSettings();
    Evaluator.Settings = settings;
    Evaluator.Evaluate(expression);
    return Evaluator.LastResult;
}

テストは優れていますが、テスト対象のコードのAPIは不良です。アダプターレイヤーを更新するだけで、テストを変更せずにリファクタリングできます。

static object Eval(string expression)
{
    // After refactoring...
    var settings = new EvaluatorSettings();
    var evaluator = new Evaluator(settings);
    return evaluator.Evaluate(expression);
}

この例は、自分自身を繰り返さないという原則に基づいて行うべき非常に明白なことのように思えますが、他の場合にはそれほど明白ではないかもしれません。利点はDRYを超えています-実際の利点は、テスト対象のコードからテストを切り離すことです。

もちろん、この手法はすべての状況で推奨されるとは限りません。たとえば、POCO / POJO用のアダプターを作成する理由はありません。テストコードとは無関係に変更できるAPIが実際にはないからです。また、少数のテストを記述している場合、比較的大きなアダプターレイヤーはおそらく無駄になります。

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