私の現在のプロジェクトは、簡潔に「制約のあるランダムなイベント」の作成に関係しています。私は基本的に検査のスケジュールを生成しています。それらのいくつかは、厳密なスケジュールの制約に基づいています。金曜日の午前10:00に週に1回検査を行います。他の検査は「ランダム」です。「検査は週に3回行う必要がある」、「検査は午前9時から午後9時までの間に行う必要がある」、「同じ8時間内に2つの検査を行うべきではない」などの基本的な構成可能な要件がありますが、特定の検査セットに対して設定された制約の範囲内で、結果の日付と時刻は予測できません。
ユニットテストとTDD、IMOは、このシステムで大きな価値があります。これは、要件の完全なセットがまだ不完全でありながら、段階的にビルドするために使用できるためです。現在、私が必要だとは知らない。厳密なスケジュールは、TDDにとって欠かせないものでした。ただし、システムのランダムな部分のテストを作成するときに、テスト対象を実際に定義するのは難しいと感じています。スケジューラによって生成されるすべての時間は制約内に収まる必要があると断言できますが、実際の時間を非常に「ランダム」にせずにこのようなすべてのテストに合格するアルゴリズムを実装できます。実際、それはまさに起こったことです。時刻は、正確には予測できませんが、許容される日付/時刻範囲の小さなサブセットに分類される問題を発見しました。アルゴリズムは、私が合理的に行うことができると思うすべてのアサーションに合格し、そのような状況で失敗する自動テストを設計することはできませんでしたが、「よりランダムな」結果が与えられると合格します。既存のテストを再構築して何度も繰り返すことで問題が解決したことを実証し、生成された時間が完全に許容範囲内に収まったことを視覚的に確認する必要がありました。
非決定論的な動作を期待するテストを設計するためのヒントはありますか?
すべての提案に感謝します。主な意見は、決定論的で、再現性があり、主張可能な結果を得るために、決定論的テストが必要だということです。理にかなっています。
制約プロセス(任意の長いバイト配列が最小と最大の間で長くなるプロセス)の候補アルゴリズムを含む一連の「サンドボックス」テストを作成しました。次に、そのコードをFORループで実行し、アルゴリズムにいくつかの既知のバイト配列(開始時に1から10,000,000の値)を与え、アルゴリズムにそれぞれ1009から7919の間の値に制約させます(素数を使用して、アルゴリズムは、入力範囲と出力範囲の間で偶然のGCFを通過しません)。結果の制約値がカウントされ、ヒストグラムが作成されます。「パス」するには、すべての入力をヒストグラム内に反映する必要があり(「失わない」ようにするための健全性)、ヒストグラム内の2つのバケットの差は2より大きくすることはできません(実際には<= 1でなければなりません) 、しばらくお待ちください)。勝ったアルゴリズムがあれば、それをカットして実動コードに直接貼り付け、永続的なテストを実施して回帰することができます。
コードは次のとおりです。
private void TestConstraintAlgorithm(int min, int max, Func<byte[], long, long, long> constraintAlgorithm)
{
var histogram = new int[max-min+1];
for (int i = 1; i <= 10000000; i++)
{
//This is the stand-in for the PRNG; produces a known byte array
var buffer = BitConverter.GetBytes((long)i);
long result = constraintAlgorithm(buffer, min, max);
histogram[result - min]++;
}
var minCount = -1;
var maxCount = -1;
var total = 0;
for (int i = 0; i < histogram.Length; i++)
{
Console.WriteLine("{0}: {1}".FormatWith(i + min, histogram[i]));
if (minCount == -1 || minCount > histogram[i])
minCount = histogram[i];
if (maxCount == -1 || maxCount < histogram[i])
maxCount = histogram[i];
total += histogram[i];
}
Assert.AreEqual(10000000, total);
Assert.LessOrEqual(maxCount - minCount, 2);
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionMSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByMSBRejection);
}
private long ConstrainByMSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length-1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Apply a bitmask to the value, removing the MSB on each loop until it falls in the range.
var mask = long.MaxValue;
while (result > max - min)
{
mask >>= 1;
result &= mask;
}
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionLSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByLSBRejection);
}
private long ConstrainByLSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length - 1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Bit-shift the number 1 place to the right until it falls within the range
while (result > max - min)
result >>= 1;
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionModulus()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByModulo);
}
private long ConstrainByModulo(byte[] buffer, long min, long max)
{
buffer[buffer.Length - 1] &= 0x7f;
var result = BitConverter.ToInt64(buffer, 0);
//Modulo divide the value by the range to produce a value that falls within it.
result %= max - min + 1;
result += min;
return result;
}
...そして結果は次のとおりです。
LSB拒否(番号が範囲内に入るまでビットシフトする)は、非常にわかりやすい理由で、ひどいものでした。最大値未満になるまで任意の数を2で除算すると、すぐに終了します。重要な範囲ではない場合は、結果が上位3分の1に偏ります(ヒストグラムの詳細な結果に見られるように) )。これはまさに完成した日付から見た振る舞いでした。すべての時間は非常に特定の日の午後でした。
MSB拒否(範囲内になるまで一度に1つずつ最上位ビットを削除する)の方が優れていますが、繰り返しますが、各ビットで非常に大きな数を切り落とすため、均等に分散されません。上端と下端に数字が表示される可能性は低いため、中央の3分の1にバイアスがかかります。これは、ランダムデータを釣鐘曲線に「正規化」したい人にとっては有益かもしれませんが、2つ以上の小さな乱数の合計(サイコロを投げるのに似ています)は、より自然な曲線を与えます。私の目的では、失敗します。
このテストに合格した唯一の方法は、モジュロ除算によって制約することでした。これは、3つのうちで最速であることが判明しました。Moduloは、その定義により、利用可能な入力が与えられると、できるだけ均等な分布を生成します。