単体テストでの循環的な依存関係との闘い


24

私はTDDを使って、Bit Vectorのような単純なものを開発することで、TDDを実践しようとしています。私はたまたまSwiftを使用していますが、これは言語に依存しない質問です。

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

これまでのところ、非常にシンプルな機能がいくつかあります。

  1. 空のビットベクトルを構築する初期化子
  2. countタイプのプロパティInt
  3. isEmptyタイプのプロパティBool
  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(aとしてUInt64)既存のビットパターンからを構築するプライベートAPIを導入することにより、この問題を部分的に解決することができました。これにより、他のイニシャライザーをテストせずに値を初期化できたので、「ブートストラップ」することができました。

ユニットテストが本当にユニットテストであるためには、たくさんのハックをしていることに気付きます。

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


20
あなたは「ユニット」という用語についてあまりにも狭い視野を取っています。BitVectorは単体テストに最適なユニットサイズでBitVectorあり、公共のメンバーが有意義なテストを行うために必要な問題を即座に解決します。
バートファンインゲンシェナウ

実装の詳細を前もって知っています。開発は本当にテスト駆動ですか?
ハービー

@herbyいいえ、だから私は練習しています。それは本当に達成不可能な標準のように思えますが。実装が何を伴うかについてのかなり明確な精神的近似なしに、私が何かをプログラミングしたことはありません。
アレクサンダー-モニカの復活

@Alexanderそれを緩和しようとする必要があります。そうしないと、テスト主導型ではなくテスト優先になります。あいまいな「バッキングストアとして1つの64ビットintを使用してビットベクトルを作成します」と言うだけです。その時点から、TDD red-green-refactorを次々に実行します。実装の詳細とAPIは、テストを実行しようとすること(前者)、および最初にテストを作成すること(後者)から明らかになるはずです。
ハービー

回答:


66

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

それは問題ではないことを、あなたの現在の実装ではisEmptyに依存しているcount(またはあなたが持つかもしれない他のどのような関係):あなたが気にしなければならないすべては公開インタフェースです。たとえば、次の3つのテストを実行できます。

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

これらはすべて有効なテストであり、クラスの内部をリファクタリングisEmptyして依存しない別の実装を行うことにした場合に特に重要になりますcount-テストがすべて合格する限り、回帰していないことがわかります何でも。

同様のことが他のポイントにも当てはまります。内部実装ではなく、パブリックインターフェイスをテストすることを忘れないでください。ここでTDDが便利な場合があります。それは、TDDのisEmpty実装を作成する前に必要なテストを作成しているためです。


6
@Alexanderユニットテストの明確な定義を必要としている男性のように聞こえます。私が知っている最良のものがから来たマイケル羽
candied_orange

14
@Alexanderでは、各メソッドを独立してテスト可能なコードとして扱います。それがあなたの困難の原因です。オブジェクトをより小さな部分に分割することなく、オブジェクト全体をテストすると、これらの問題は解消されます。オブジェクト間の依存関係は、メソッド間の依存関係と比較できません。
amon

9
@Alexander「コードの一部」は任意の測定値です。変数を初期化するだけで、多くの「コード」を使用しています。重要なのは、あなたが定義した凝集行動ユニットをテストしているということです
Ant P

9
「私が読んだことから、コードの一部だけを壊すと、そのコードに直接関連する単体テストだけが失敗するという印象を受けました。」それに従うことは非常に難しいルールのようです。(たとえば、ベクトルクラスを記述し、インデックスメソッドでエラーを起こした場合、そのベクトルクラスを使用するすべてのコードで大量の破損が発生する可能性があります)
jhominal

4
@Alexanderまた、テストのために「アレンジ、アクト、アサート」パターンを調べます。基本的に、オブジェクトを必要な状態に設定し(Arrange)、実際にテストしているメソッドを呼び出し(Act)、その状態が期待どおりに変化したことを確認します。(アサート)。Arrangeで設定するものは、テストの「前提条件」になります。
ギャラクティック

5

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

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

メモリ内の可変データを管理するオブジェクトは、基本的にステートマシンです。最低でも、情報を配置するメソッドInvokeに起こっているすべての貴重なユースケースは、それほど対象外の情報のコピーを読み取るためにオブジェクト、メソッド呼び出します。興味深いユースケースでは、データ構造を変更する追加のメソッドも呼び出します。

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

// 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つのテストだけで検出される設計に投資しようとすることには、多くの利点はありません。

もちろん、マージによって障害が発生し、その障害を見つけることは簡単ではなくなります。障害を簡単に特定できるようにするためのさまざまな手順があります。見る


1

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

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

テスト自体が賢明で適切に管理されている限り、コールグラフがテストの下にどのように表示されるかは関係ありません。

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

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

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

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