AIのようなシステムを作成すると、非常に迅速に多くの異なるパスをたどることができ、実際にはいくつかの異なる入力を持つアルゴリズムであれば、可能な結果セットに多数の順列が含まれる可能性があります。
結果の多くの異なる置換を出力するシステムを作成する場合、TDDを使用するためにどのようなアプローチを取るべきですか?
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));
}
すべてのEdgeケースをテストできるように、順列を強制できるようにアプリケーションを設計する方法についてのアイデアが得られれば幸いです。
厳密なTDDは、より複雑なシステムでは少し故障する傾向がありますが、実際にはそれほど重要ではありません。個々の入力を分離できない場合は、適切なカバレッジを提供するテストケースをいくつか選択して、それらを使用してください。
これには、実装がうまく機能するための知識がある程度必要ですが、それは理論的な懸念事項です。技術者以外のユーザーが詳細に指定したAIを構築することはほとんどありません。これは、テストケースにハードコーディングすることでテストに合格するのと同じカテゴリに属します。正式にはテストが仕様であり、実装は正確であり、可能な限り最速のソリューションですが、実際には発生しません。
複雑さでばらばらになるどころか、このような状況では優れています。それはあなたがより小さな部分でより大きな問題を考慮するようにあなたを駆り立て、それはより良いデザインにつながります。
アルゴリズムのすべての順列をテストしようと試みないでください。テストごとにテストをビルドし、ベースがカバーされるまで、テストを機能させるための最も単純なコードを記述します。他の部分をテストしながら問題の一部を偽造し、100億の順列に対して100億のテストを作成する手間を省くことができるので、問題を分解することの意味がわかるはずです。
編集:例を追加したかったのですが、前に時間がありませんでした。
インプレースソートアルゴリズムについて考えてみましょう。先に進んで、配列の上端、配列の下端、および真ん中のあらゆる種類の奇妙な組み合わせをカバーするテストを作成できます。それぞれについて、ある種のオブジェクトの完全な配列を作成する必要があります。これには時間がかかります。
または、4つの部分で問題に取り組むことができます。
最初の問題は問題の唯一の複雑な部分ですが、それを残りの部分から抽象化することで、はるかに簡単になりました。
2つ目はほぼ確実にオブジェクト自体によって処理されます。少なくともオプションとして、多くの静的型フレームワークでは、その機能が実装されているかどうかを示すインターフェイスがあります。したがって、これをテストする必要はありません。
3番目のテストは非常に簡単です。
4番目は、2つのポインターを処理し、トラバーサルクラスにポインターを移動するように要求し、比較を呼び出し、その比較の結果に基づいて、交換するアイテムを呼び出します。最初の3つの問題を偽装している場合は、これを非常に簡単にテストできます。
ここでどのようにしてより良いデザインに導いたのでしょうか?単純にして、バブルソートを実装したとします。機能しますが、本番環境に移行して100万個のオブジェクトを処理する必要がある場合は、非常に遅くなります。新しいトラバーサル機能を作成して入れ替えるだけで済みます。他の3つの問題の処理の複雑さに対処する必要はありません。
これが、ユニットテストとTDDの違いです。ユニットテスターは、これによりテストが脆弱になったと言います。単純な入力と出力をテストした場合は、新しい機能のためにこれ以上テストを書く必要はありません。 TDDerは、関心のあるクラスを適切に分離し、各クラスが1つのことと1つのことをうまく行うと言っています。
多くの変数を持つ計算のすべての順列をテストすることは不可能です。しかし、それは新しいことではありません。おもちゃの複雑さを超えるプログラムには常に当てはまります。テストのポイントは、計算のpropertyを検証することです。たとえば、1000の数値でリストを並べ替えるには多少の手間がかかりますが、個々のソリューションは非常に簡単に検証できます。今、1000個ありますが!そのプログラムの可能な(クラスの)入力であり、すべてをテストすることはできません。ランダムに1000入力を生成し、出力が実際にソートされていることを確認するだけで十分です。どうして?ランダムに生成された1000個のベクトルを確実にソートするプログラムを書くことはほぼ不可能であるためなしも一般に正しい(特定の魔法の入力を操作するために意図的にリギングしない限り...)
さて、一般的に物事はもう少し複雑です。本当にhaveバグがありました。ユーザー名に「f」があり、曜日が金曜日である場合、メーラーがユーザーにメールを配信しませんでした。しかし、私はそのような奇妙さを予想しようとする無駄な努力をしていると思います。テストスイートは、システムが期待する入力に対して期待どおりの動作をするという確実な信頼を提供します。特定のファンキーなケースでファンキーなことを行う場合、最初のファンキーなケースを試した後すぐに気づき、そのケースに対して特にテストを書くことができます(通常、同様のケースのクラス全体をカバーします)。
Edgeケースといくつかのランダム入力を取ります。
並べ替えの例を見てみましょう。
これらで高速に動作する場合、すべての入力で動作することを確信できます。