多くの順列を持つ何かに対してTDDを行う方法は?


15

非常に多くの異なるパスをたどることができるAIのようなシステム、または実際に複数の異なる入力を持つ任意のアルゴリズムを作成する場合、可能な結果セットには多数の順列が含まれることがあります。

結果の多くの異なる順列を出力するシステムを作成するときに、TDDを使用するにはどのようなアプローチをとるべきですか?


1
AIシステムの全体的な良さは、通常、ベンチマーク入力セットを使用したPrecision-Recallテストによって測定されます。このテストは、「統合テスト」とほぼ同等です。他の人が述べたように、それは「テスト駆動設計」ではなく、「テスト駆動アルゴリズムの研究」に似ています。
rwong

「AI」の意味を定義してください。これは、特定の種類のプログラム以上の研究分野です。特定のAI実装では、通常、TDDを介して特定の種類(つまり、緊急行動)をテストすることはできません。
スティーブンエバーズ

@SnOrfus私はそれを最も一般的で初歩的な意味での意思決定マシンという意味です。
ニコール

回答:


7

pdrの答えへのより実用的なアプローチをとる。TDDは、テストではなくソフトウェア設計に関するものです。ユニットテストを使用して、作業を進めながら検証します。

そのため、ユニットテストレベルでは、完全に決定論的な方法でテストできるようにユニットを設計する必要があります。これを行うには、ユニットを非決定的にするもの(乱数ジェネレーターなど)を取り、それを抽象化します。動きが良いかどうかを判断する方法の単純な例があるとします。

class Decider {

  public boolean decide(float input, float risk) {

      float inputRand = Math.random();
      if (inputRand > input) {
         float riskRand = Math.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider();
d.decide(0.1337f, 0.1337f);

このメソッドをテストするのは非常に難しく、単体テストで実際に検証できるのはその境界だけです... その代わりに、インターフェースと機能をラップする具体的なクラスを作成して、ランダム化部分を抽象化します。

public interface IRandom {

   public float random();

}

public class ConcreteRandom implements IRandom {

   public float random() {
      return Math.random();
   }

}

このDeciderクラスは、その抽象化を通じて具体的なクラス、つまりインターフェイスを使用する必要があります。この方法は、依存性注入と呼ばれます(以下の例はコンストラクター注入の例ですが、セッターを使用してこれを行うこともできます)。

class Decider {

  IRandom irandom;

  public Decider(IRandom irandom) { // constructor injection
      this.irandom = irandom;
  }

  public boolean decide(float input, float risk) {

      float inputRand = irandom.random();
      if (inputRand > input) {
         float riskRand = irandom.random();
      }
      return false;

  }

}

// The usage:
Decider d = new Decider(new ConcreteRandom);
d.decide(0.1337f, 0.1337f);

この「コードの肥大化」が必要な理由を自問するかもしれません。まず、アルゴリズムのランダム部分の動作をモックできるDeciderようになりましたIRandom。これは、sの「契約」に続く依存関係があるためです。このためにモックフレームワークを使用できますが、この例は自分でコーディングするのに十分簡単です。

class MockedRandom() implements IRandom {

    public List<Float> floats = new ArrayList<Float>();
    int pos;

   public void addFloat(float f) {
     floats.add(f);
   }

   public float random() {
      float out = floats.get(pos);
      if (pos != floats.size()) {
         pos++;
      }
      return out;
   }

}

最良の部分は、これが「実際の」具体的な実装を完全に置き換えることができるということです。コードは次のようにテストしやすくなります。

@Before void setUp() {
  MockedRandom mRandom = new MockedRandom();

  Decider decider = new Decider(mRandom);
}

@Test
public void testDecisionWithLowInput_ShouldGiveFalse() {

  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandButLowRiskRand_ShouldGiveFalse() {

  mRandom.addFloat(1f);
  mRandom.addFloat(0f);

  assertFalse(decider.decide(0.1337f, 0.1337f));
}

@Test
public void testDecisionWithHighInputRandAndHighRiskRand_ShouldGiveTrue() {

  mRandom.addFloat(1f);
  mRandom.addFloat(1f);

  assertTrue(decider.decide(0.1337f, 0.1337f));
}

これにより、すべてのエッジケースなどをテストできるように順列を強制できるように、アプリケーションの設計方法に関するアイデアが得られることを願っています。


3

厳密なTDDは、より複雑なシステムでは少し壊れる傾向がありますが、実際の用語ではそれほど重要ではありません-個々の入力を分離できるようになったら、妥当なカバレッジを提供するテストケースをいくつか選択して使用します。

これには実装がうまくいくかどうかのある程度の知識が必要ですが、それはより理論的な懸念です-非技術ユーザーによって詳細に指定されたAIを構築することはほとんどありません。これは、テストケースにハードコーディングしてテストを渡すのと同じカテゴリに属します。公式には、テストは仕様であり、実装は正しく、可能な限り高速なソリューションですが、実際には起こりません。


2

TDDはテストではなく、設計です。

複雑さでバラバラになることからほど遠く、これらの状況で優れています。それは、より小さなデザインでより大きな問題を検討するようにあなたを駆り立て、それはより良いデザインにつながるでしょう。

アルゴリズムのすべての順列をテストしようとしないでください。テストの後にテストをビルドし、ベースがカバーされるまで、テストを機能させる最も簡単なコードを記述します。他の部分をテストするときに問題の一部を偽造することをお勧めします。これにより、100億の順列に対して100億のテストを書く必要がなくなります。

編集:例を追加したかったのですが、時間がありませんでした。

インプレースソートアルゴリズムを考えてみましょう。先に進み、配列の上端、配列の下端、および中間のあらゆる種類の奇妙な組み合わせをカバーするテストを作成できます。それぞれについて、何らかのオブジェクトの完全な配列を作成する必要があります。これには時間がかかります。

または、次の4つの部分で問題に取り組むことができます。

  1. 配列を走査します。
  2. 選択したアイテムを比較します。
  3. アイテムを切り替えます。
  4. 上記の3つを調整します。

最初の部分は問題の唯一の複雑な部分ですが、残りの部分からそれを抽象化することで、はるかに簡単になりました。

2番目はほとんど確実にオブジェクト自体によって処理されます。少なくともオプションとして、多くの静的型付きフレームワークでは、その機能が実装されているかどうかを示すインターフェイスがあります。したがって、これをテストする必要はありません。

3番目は非常に簡単にテストできます。

4番目は2つのポインターを処理し、トラバーサルクラスにポインターを移動するように要求し、比較を呼び出し、その比較の結果に基づいて、スワップされるアイテムを呼び出します。最初の3つの問題を偽装した場合は、非常に簡単にテストできます。

ここで、より良いデザインにどのように導いたのでしょうか?あなたはそれをシンプルに保ち、バブルソートを実装したとしましょう。動作しますが、実稼働環境に移動し、100万個のオブジェクトを処理する必要がある場合は、非常に遅くなります。あなたがしなければならないのは、新しいトラバーサル機能を書き、それを交換することです。他の3つの問題を処理する複雑さを扱う必要はありません。

これは、単体テストとTDDの違いです。ユニットテスターは、これによりテストが脆弱になり、単純な入力と出力をテストした場合、新しい機能のためにテストを記述する必要がなくなると言います。TDDerは、私が持っている各クラスが1つのことと1つのことをうまく行えるように、懸念を適切に分離したと言います。


1

多くの変数を使用して計算のすべての順列をテストすることはできません。しかし、それは新しいことではなく、おもちゃの複雑さを超えるプログラムには常に当てはまります。テストのポイントは、計算のプロパティを確認することです。たとえば、1000個の数字でリストを並べ替えるには多少の手間がかかりますが、個々のソリューションは非常に簡単に検証できます。今、1000がありますが!そのプログラムの可能な(クラスの)入力であり、すべてをテストすることはできません。1000個の入力をランダムに生成し、出力が実際にソートされていることを検証するだけで十分です。どうして?一般的に正しいことなく、ランダムに生成された1000個のベクトルを確実にソートするプログラムを書くことはほぼ不可能であるため(特定のマジック入力を操作するために意図的にリグしない限り...)

さて、一般的に物事はもう少し複雑です。実際にそこにいる彼らは、ユーザー名に「F」を持っていると曜日が金曜日であればメーラーは、ユーザーにメールを配信しないだろうバグがありました。しかし、私はそのような奇妙さを予測しようとする無駄な努力だと思います。テストスイートは、システムが期待する入力に対して期待どおりの動作をするという確実な自信を提供する必要があります。特定のファンキーなケースでファンキーなことをする場合、最初のファンキーなケースを試した後すぐに気づき、そのケースに対してテストを書くことができます(通常、同様のケースのクラス全体をカバーします)。


1000個の入力をランダムに生成した場合、どのように出力をテストしますか?確かに、このようなテストには、それ自体はテストされていないロジックが含まれます。あなたはテストをテストしますか?どうやって?ポイントは、状態遷移を使用してロジックをテストする必要があることです-入力Xが与えられた場合、出力はYである必要があります。論理的に言えば、引数を別の引数で正当化すると、懐疑的な回帰パスになります。いくつかの主張をしなければなりません。これらのアサーションはテストです。
イザキ

0

エッジケースに加えて、ランダムな入力を行います。

ソートの例を見るには:

  • いくつかのランダムリストを並べ替える
  • 既にソートされているリストを取得する
  • 逆順のリストを取る
  • ほぼソートされたリストを取ります

これらに対して高速に動作する場合、すべての入力に対して動作することを確信できます。

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