指数関数的なテストケースが必要なTDDおよび完全なテストカバレッジ


17

クライアントからの非常に特定の要件ごとに、検索結果の順序付けられていないリストのソートを支援するために、リストコンパレータに取り組んでいます。要件では、重要度の順に次のルールを使用してランク付けされた関連性アルゴリズムが必要です。

  1. 名前の完全一致
  2. 検索クエリのすべての単語の名前または結果の同義語
  3. 検索クエリの一部の単語の名前または結果の同義語(%降順)
  4. 説明内の検索クエリのすべての単語
  5. 説明内の検索クエリの一部の単語(%降順)
  6. 最終更新日が降順

このコンパレータの自然なデザインの選択は、2の累乗に基づいてスコア付けされたランキングであるように思われました。重要度の低いルールの合計は、重要度の高いルールの肯定的な一致を超えることはありません。これは、次のスコアによって達成されます。

  1. 32
  2. 16
  3. 8(降順%に基づく2次タイブレーカースコア)
  4. 4
  5. 2(降順%に基づく2次タイブレーカースコア)
  6. 1

TDDの精神で、私は最初にユニットテストから始めることにしました。一意のシナリオごとにテストケースを作成することは、ルール3および5のセカンダリタイブレーカーロジックの追加のテストケースを考慮せずに、少なくとも63の一意のテストケースになります。これは耐えがたいようです。

ただし、実際のテストは実際には少なくなります。実際のルール自体に基づいて、特定のルールにより、下位のルールが常に真になることが保証されます(たとえば、「すべての検索クエリワードが説明に表示される」場合、ルール「一部の検索クエリワードが説明に表示される」は常に真になります)。それでも、これらの各テストケースを書き出す努力のレベルは価値がありますか?これは、TDDで100%のテストカバレッジについて話すときに通常要求されるテストのレベルですか?そうでない場合、許容可能な代替テスト戦略は何でしょうか?


1
このシナリオと同様のシナリオが、テストコードを1回記述し、入力と期待される結果を含む2つ以上の配列にフィードできる「TMatrixTestCase」と列挙子を開発した理由です。
マルジャンヴェネマ

回答:


16

あなたの質問は、TDDが「すべてのテストケースを最初に書く」ことと関係があることを暗示しています。私見は「TDDの精神」ではなく、実際はそれに反しています。TDDは「テスト駆動開発」の略であるため、必要なのは実装を実際に「駆動」するテストケースだけであり、それ以上は必要ありません。また、新しい要件ごとにコードブロックの数が指数関数的に増加するように実装が設計されていない限り、テストケースの指数関数的な数も必要ありません。あなたの例では、TDDサイクルはおそらく次のようになります。

  • リストの最初の要件から開始します。「名前と完全に一致」の単語は、他のすべてよりも高いスコアを取得する必要があります
  • 次に、このための最初のテストケース(たとえば、指定されたクエリに一致する単語)を作成し、そのテストに合格する最小限の作業コードを実装します。
  • 最初の要件に2番目のテストケース(クエリに一致しない単語など)を追加し、新しいテストケースを追加する前に、2番目のテストに合格するまで既存のコードを変更します
  • 実装の詳細に応じて、空のクエリ、空の単語などのテストケースを自由に追加してください(TDDはホワイトボックスアプローチであるため、実装を知っているという事実を利用できますテストケースを設計します)。

次に、2番目の要件から始めます。

  • 「名前に含まれる検索クエリのすべての単語または結果の同義語」は、「名前に完全一致」よりも低いスコアを取得する必要がありますが、他のすべてよりも高いスコアを取得する必要があります。
  • 上記のように、この新しい要件のテストケースを次々に構築し、新しいテストのたびにコードの次の部分を実装します。コードとテストケースの間にリファクタリングすることを忘れないでください。

ここで問題が発生します。要件/カテゴリ番号「n」のテストケースを追加する場合、カテゴリ「n-1」のスコアがカテゴリ「n」のスコアよりも高いことを確認するためのテストを追加するだけです。 。カテゴリ1、...、n-1の他のすべての組み合わせに対してテストケースを追加する必要はありません。前に書いたテストにより、そのカテゴリのスコアが正しい順序であることが確認されます。

したがって、これにより、要件の数に対して指数関数的にではなく、ほぼ線形に成長する多くのテストケースが得られます。


私はこの答えが本当に好きです。TDDを念頭に置いて、この問題に取り組むための明確で簡潔な単体テスト戦略を提供します。あなたはそれを非常にうまく分解します。
maple_shaft

@maple_shaft:ありがとう、あなたの質問が本当に好きです。最初にすべてのテストケースを設計するというアプローチでも、テスト用の同等クラスを構築する古典的な手法で指数関数的成長を減らすのに十分かもしれないと付け加えたいと思います(しかし、私はこれまでのところうまくいきませんでした)。
ドックブラウン14年

13

事前定義された条件のリストを調べて、チェックが成功するたびに現在のスコアに2を掛けるクラスを作成することを検討してください。

これは、いくつかの模擬テストを使用して非常に簡単にテストできます。

その後、各条件にクラスを記述できます。各ケースには2つのテストしかありません。

私はあなたのユースケースを本当に理解していませんが、うまくいけばこの例が役立つでしょう。

public class ScoreBuilder
{
    private ISingleScorableCondition[] _conditions;
    public ScoreBuilder (ISingleScorableCondition[] conditions)
    {
        _conditions = conditions;
    }

    public int GetScore(string toBeScored)
    {
        foreach (var condition in _conditions)
        {
            if (_conditions.Test(toBeScored))
            {
                // score this somehow
            }
        }
    }
}

public class ExactMatchOnNameCondition : ISingleScorableCondition
{
    private IDataSource _dataSource;
    public ExactMatchOnNameCondition(IDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public bool Test(string toBeTested)
    {
        return _dataSource.Contains(toBeTested);
    }
}

// etc

2 ^ conditionsテストはすぐに4+(2 * conditions)になります。20は64よりもはるかに劣りません。後で別のクラスを追加する場合、既存のクラスを変更する必要はありません(オープンクローズの原則)。したがって、64の新しいテストを記述する必要はありません。 2つの新しいテストを含む別のクラスを追加し、それをScoreBuilderクラスに挿入します。


興味深いアプローチ。単一のコンパレータコンポーネントを念頭に置いていたので、私の心はずっとOOPアプローチを考えませんでした。私はアルゴリズムのアドバイスを本当に探していませんでしたが、これは関係なく非常に役立ちます。
maple_shaft

4
@maple_shaft:いいえ。ただし、TDDのアドバイスを探していました。この種のアルゴリズムは、労力を大幅に削減することで、労力に値するかどうかの問題を解決するのに最適です。複雑さを減らすことがTDDの鍵です。
pdr

+1、素晴らしい答え。このような高度なソリューションがなくても、テストケースの数を指数関数的に増やす必要はないと考えています(以下の回答を参照)。
ドックブラウン14年

別の回答が実際の質問にうまく対応していると感じたため、あなたの答えを受け入れませんでしたが、私はあなたの設計アプローチがとても気に入ったので、あなたが提案した方法でそれを実装しています。これにより、複雑さが軽減され、長期的には拡張性が高まります。
maple_shaft

4

それでも、これらの各テストケースを書き出す努力のレベルはそれだけの価値がありますか?

「価値がある」と定義する必要があります。この種のシナリオの問題は、テストの有用性が低下することです。確かに、最初に書くテストはまったく価値があります。優先順位の明らかなエラー、および単語を分割しようとするときの構文解析エラーなどを見つけることができます。

2番目のテストは、コードを通る別のパスをカバーし、おそらく別の優先順位関係をチェックするため、価値があります。

63番目のテストは、99.99%がコードのロジックまたは別のテストでカバーされていると確信しているため、おそらく価値がありません。

これは、TDDで100%のテストカバレッジについて話すときに通常要求されるテストのレベルですか?

私の理解では、100%のカバレッジは、すべてのコードパスが実行されることを意味します。これは、ルールのすべての組み合わせを行うことを意味するものではありませんが、コードがダウンする可能性のあるすべての異なるパスを指します(指摘するように、一部の組み合わせはコードに存在できません)。ただし、TDDを実行しているため、パスを確認するための「コード」はまだありません。プロセスの手紙は、63 +すべてを作ると言うでしょう。

個人的には、100%の報道は夢のようなものだと思います。それを超えて、それは非実用的です。ユニットテストはあなたに役立つために存在し、その逆ではありません。より多くのテストを行うと、利益(テストがバグを防ぐ可能性+コードが正しいという確信)に対する利益が減少します。コードの実行内容に応じて、スライドスケールのどこでテストの実行を停止するかを定義します。コードで原子炉を実行している場合は、63以上のすべてのテストに価値があります。コードが音楽アーカイブを整理している場合は、おそらくもっと少ないもので済ますことができます。


「カバレッジ」とは、通常、コードカバレッジ(コードのすべての行が実行される)またはブランチカバレッジ(すべての可能な方向でブランチが少なくとも1回実行される)を指します。どちらのタイプのカバレッジでも、64種類のテストケースは必要ありません。少なくとも、64の各ケースの個々のコード部分を含まない深刻な実装ではありません。したがって、100%のカバレッジが完全に可能です。
Doc Brown 14年

@DocBrown-確かに、この場合-他のものはテストするのが難しい/不可能です。メモリ不足の例外パスを検討してください。実装を知らずに動作をテストするために、「手紙で」TDDで64個すべてが必要ではないでしょうか?
テラスティン14年

まあ、私のコメントは質問に関連していたので、あなたの答えは、OPの場合に 100%のカバレッジを得ることが難しいかもしれないという印象を与えます。私はそれを疑います。そして、100%のカバレッジを達成するのが難しいケースを作成できることに同意しますが、それは尋ねられませんでした。
ドックブラウン14年

4

これはTDDの完璧なケースだと私は主張します。

テストする既知の基準セットがあり、それらのケースの論理的な内訳があります。ユニットテストを今または後で行うと仮定すると、既知の結果を取得してその結果に基づいて構築し、実際には各ルールを個別にカバーしていることが保証されます。

さらに、新しい検索ルールを追加すると既存のルールに違反するかどうかを確認できます。コーディングの最後にこれらをすべて行うと、おそらく、1つを修正するために1つを変更する必要があり、それが別のものを壊し、別のものを壊すという大きなリスクを冒します...または微調整が必​​要です。


1

私は、100%のテストカバレッジを、すべての単一メソッドに対する仕様の記述や、コードのすべての順列のテストとして厳密に解釈することを好むわけではありません。これを熱狂的に行うと、ビジネスロジックが適切にカプセル化されず、サポートされているビジネスロジックを記述するという意味では一般に意味のないテスト/仕様を生成する、テスト駆動型のクラス設計につながる傾向があります。代わりに、ビジネスルール自体のようにテストを構造化することに焦点を当て、テストが一般的なユースケースと同様にテスターに​​よって容易に理解され、実際に説明されることを明示的に期待して、テストでコードのすべての条件分岐を実行するよう努めます実装されたビジネスルール。

この考えを念頭に置いて、リストされた6つのランキングファクターを互いに独立して単体テストし、その後、結果を期待される総合ランキング値にロールアップすることを確認する2または3つの統合スタイルテストを行います。たとえば、ケース#1、名前の完全一致、正確な場合とそうでない場合、および2つのシナリオが期待されるスコアを返すことをテストするために、少なくとも2つの単体テストがあります。大文字と小文字が区別される場合、「完全一致」と「完全一致」、および場合によっては句読点、余分なスペースなどのその他の入力バリエーションをテストする場合も、期待されるスコアを返します。

ランキングスコアに寄与する個々の要因をすべて調べたら、これらが統合レベルで正しく機能していることを基本的に想定し、それらの組み合わされた要因が最終的な予想ランキングスコアに正しく寄与することを確認します。

ケース#2 /#3と#4 /#5が同じ基本メソッドに一般化されているが、異なるフィールドを渡す場合、基本メソッドのユニットテストを1セットだけ記述し、特定のフィールド(タイトル、名前、説明など)および指定されたファクタリングでのスコアリング。これにより、テスト作業全体の冗長性がさらに削減されます。

このアプローチでは、上記のアプローチでは、おそらくケース#1で3または4のユニットテストが行​​われ、おそらく類義語を含む一部またはすべての10の仕様に加えて、ケース#2-#5および2の正しいスコアリングに関する4つの仕様が得られます最終日の順序付けされたランキングで3仕様に、その後、可能性のある方法で組み合わされた6ケースすべてを測定する3から4統合レベルテストその条件は処理されます)、または後のリビジョンで違反/破損が発生しないことを確認ます。これにより、記述されたコードの100%を実行するために約25程度の仕様が得られます(記述されたメソッドの100%を直接呼び出していない場合でも)。


1

私は100%のテストカバレッジのファンではありませんでした。私の経験では、1つまたは2つのテストケースだけでテストできるほど単純なものであれば、失敗することはほとんどありません。失敗した場合、通常はテストの変更を必要とするアーキテクチャの変更が原因です。

そうは言っても、あなたのような要件については、誰も私を作っていない個人的なプロジェクトであっても、ユニットテストは常に徹底的にユニットテストを行います。何かをテストするために必要な単体テストが多いほど、単体テストの時間を節約できます。

それは、一度にたくさんの物を頭の中でしか持てないからです。63の異なる組み合わせで動作するコードを作成しようとしている場合、1つの組み合わせを別の組み合わせを壊さずに修正するのは難しい場合があります。他の組み合わせを何度も手動でテストすることになります。手動テストははるかに遅いため、変更を加えるたびに可能なすべての組み合わせを再実行する必要はありません。そのため、何かを見逃す可能性が高くなり、すべてのケースで機能しないパスを追跡する時間を無駄にする可能性が高くなります。

手動テストに比べて時間を節約できるだけでなく、精神的な負担がはるかに少ないため、回帰を誤って導入することを心配することなく、問題に集中することが容易になります。これにより、燃え尽きることなく、より速く、より長く作業できます。私の意見では、時間を節約できなかったとしても、メンタルヘルスのメリットだけでも、複雑なコードの単体テストのコストに見合う価値があります。

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