web-dev-qa-db-ja.com

単体テストで周期的な依存関係に苦労する

私はTDDを使って、ビットベクタのようなシンプルなものを開発することを試みています。私はたまたまSwiftを使用していますが、これは言語に依存しない質問です。

私のBitVectorは、単一の_UInt64_を格納するstructであり、それをコレクションのように扱うためのAPIを提供します。詳細はそれほど重要ではありませんが、非常に簡単です。上位57ビットはストレージビットで、下位6ビットは「カウント」ビットであり、実際に含まれる値を格納するストレージビットの数を示します。

これまでのところ、私は非常にシンプルな機能をいくつか持っています。

  1. 空のビットベクトルを構築する初期化子
  2. タイプcountIntプロパティ
  3. タイプisEmptyBoolプロパティ
  4. 等価演算子(_==_)。注意:これは、JavaのObject.equals()に似た値等価演算子であり、Javaの_==_のような参照等価演算子ではありません。

私は循環依存の束に出くわしています:

  1. 初期化子をテストする単体テストでは、新しく構築されたBitVectorを確認する必要があります。次の3つの方法のいずれかでそれを行うことができます。

    1. チェック_bv.count == 0_
    2. チェック_bv.isEmpty == true_
    3. 確認してください_bv == knownEmptyBitVector_

    方法1はcountに依存し、方法2はisEmptyに依存します(それ自体はcountに依存するため、それを使用しても意味がありません)、方法3は_==_に依存します。いずれにしても、イニシャライザを単独でテストすることはできません。

  2. countのテストは何かを操作する必要があり、それによって必然的にイニシャライザがテストされます

  3. isEmptyの実装はcountに依存しています

  4. _==_の実装はcountに依存しています。

既存のビットパターンからBitVectorを(_UInt64_として)構築するプライベートAPIを導入することで、この問題を部分的に解決することができました。これにより、他のイニシャライザをテストせずに値を初期化できるため、自分の道を「ブートストラップ」できます。

私の単体テストが本当に単体テストであるために、私は自分の製品とテストコードを大幅に複雑にするハックの束を実行していることに気づきました。

この種の問題をどのように正確に回避しますか?

実装の詳細について心配しすぎています。

現在の実装ではisEmptycount(または他のあらゆる関係)に依存しているということは問題ではありません。パブリックインターフェイス。たとえば、次の3つのテストを行うことができます。

  • 新しく初期化されたオブジェクトにcount == 0
  • 新しく初期化されたオブジェクトにisEmpty == true
  • 新しく初期化されたオブジェクトが既知の空のオブジェクトと等しいこと。

これらはすべて有効なテストであり、isEmptycountに依存しない別の実装を持つようにクラスの内部をリファクタリングすることを決定した場合に特に重要になります。テストはすべて成功し、何も後退していないことがわかります。

同様のことが他のポイントにも当てはまります。内部実装ではなく、パブリックインターフェイスをテストしてください。 TDDは、実装を記述する前に、isEmptyに必要なテストを記述しているので、ここで役立ちます。

66
Philip Kendall

この種の問題をどのように正確に回避しますか?

「単体テスト」とは何かについての考えを修正します。

メモリ内の変更可能なデータを管理するオブジェクトは、基本的にステートマシンです。したがって、価値のあるユースケースでは、少なくとも、メソッドを呼び出して情報をオブジェクトに入れ、メソッドを呼び出して、オブジェクトからの情報。興味深いユースケースでは、データ構造を変更する追加のメソッドも呼び出すことになります。

実際には、これは次のようになります

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

または

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

「単体テスト」の用語-まあ、それはあまり良くないという長い歴史があります。

私はそれらを単体テストと呼んでいますが、受け入れられている単体テストの定義とはよく一致していません-Kent Beck、例によるテスト駆動開発

ケントはSUnitの最初のバージョンを 1994 に書きました。JUnitへの移植は1998年で、TDDブックの最初のドラフトは2002年の初めです。

これらのテスト(より正確には「プログラマーテスト」または「開発者テスト」と呼ばれます)の重要なアイデアは、テストが互いに分離されていることです。テストは変更可能なデータ構造を共有しないため、同時に実行できます。ソリューションを正しく測定するために、テストを特定の順序で実行する必要があるという心配はありません。

これらのテストの主な使用例は、プログラマーが自分のソースコードを編集するまでの間に実行されることです。赤緑のリファクタリングプロトコルを実行している場合、予期しないREDは常に最後の編集でエラーを示しています。その変更を元に戻し、テストが緑色であることを確認して、再試行します。 1つだけのテストですべての可能なバグが検出される設計に投資しようとすることには、多くの利点はありません。

もちろん、mergeはフォールトを導入し、そのフォールトはもはや自明ではありません。障害を簡単に特定できるようにするために実行できるさまざまな手順があります。見る

5
VoiceOfUnreason

一般に(TDDを使用していない場合でも)、実装方法がわからないふりをしながら、できるだけ多くのテストを作成するように努める必要があります。

TDDを実際に行っている場合は、すでにそうなっているはずです。テストはプログラムの実行可能な仕様です。

テスト自体が賢明でよく維持されている限り、コールグラフがテストの下にどのように表示されるかは重要ではありません。

あなたの問題はTDDの理解にあると思います。

私の意見におけるあなたの問題は、あなたがTDDペルソナを「混合」しているということです。 「テスト」、「コード」、および「リファクタリング」ペルソナは、理想的には互いに完全に独立して動作します。特に、コーディングとリファクタリングのペルソナは、テストをグリーンに実行/維持する以外に、テストに対する義務はありません。

確かに、原則として、すべてのテストが直交し、互いに独立しているのが最善です。しかし、それは他の2つのTDDペルソナの懸念事項ではなく、テストの厳密な、または必ずしも現実的なハード要件ではありません。基本的に:コード品質についての常識を捨てて、誰からも求められていない要件を満たそうとしないでください。

1
Tim Seguine