これは言語やフレームワークに固有のものではないと思いますが、xUnit.netとC#を使用しています。
特定の範囲のランダムな日付を返す関数があります。日付を渡しますが、返される日付は常に指定された日付の1〜40年前です。
これを単体テストするための良い方法があるかどうか、私はただ疑問に思います。最善のアプローチは、ループを作成して関数を100回実行することであり、これらの100の結果のすべてが望ましい範囲にあると主張することです。これが私の現在のアプローチです。
また、ランダムジェネレーターを制御できない限り、完全な解決策はありません(結局のところ、結果ISランダム))。しかし、特定の範囲でランダムな結果を返す機能をテストするには?
関数が目的の範囲の日付を返すことをテストすることに加えて、結果が適切に分散されていることを確認する必要があります。あなたが説明するテストは、あなたが送った日付を単に返す関数をパスするでしょう!
したがって、関数を複数回呼び出して結果が望ましい範囲内にあることをテストすることに加えて、おそらく結果をバケットに入れて、バケットの結果がほぼ同じ数であることを確認して、分布を評価しようとします完了しました。安定した結果を得るには100以上の呼び出しが必要な場合がありますが、これは高価な(実行時の)関数のように聞こえないため、数K回の反復で簡単に実行できます。
私は以前、不均一な「ランダム」関数で問題を抱えていました。それらは本当の痛みになる可能性があるので、早期にテストする価値があります。
乱数ジェネレーターを模擬または偽造する
このようなことをします...私はそれをコンパイルしなかったので、いくつかの構文エラーがあるかもしれません。
public interface IRandomGenerator
{
double Generate(double max);
}
public class SomethingThatUsesRandom
{
private readonly IRandomGenerator _generator;
private class DefaultRandom : IRandomGenerator
{
public double Generate(double max)
{
return (new Random()).Next(max);
}
}
public SomethingThatUsesRandom(IRandomGenerator generator)
{
_generator = generator;
}
public SomethingThatUsesRandom() : this(new DefaultRandom())
{}
public double MethodThatUsesRandom()
{
return _generator.Generate(40.0);
}
}
テストでは、IRandomGeneratorを偽造または模造して、缶詰のものを返します。
テストするこの問題には3つの異なる側面があると思います。
最初のもの:私のアルゴリズムは正しいものですか?つまり、適切に機能する乱数ジェネレーターが与えられた場合、範囲全体にランダムに分布する日付が生成されますか?
2つ目:アルゴリズムはEdgeケースを適切に処理しますか?つまり、乱数ジェネレーターが許容値の最大値または最小値を生成すると、何かが壊れますか?
3つ目:アルゴリズムの実装は機能していますか?つまり、疑似ランダム入力の既知のリストが与えられた場合、それは疑似ランダム日付の期待されるリストを生成しますか?
最初の2つは、単体テストスイートに組み込むものではありません。これらは、システムを設計しているときに証明するものです。 daniel.rikowskiが示唆したように、私はおそらくこれを実行して、無数の日付を生成し、カイ2乗検定を実行するテストハーネスを作成します。また、このテストハーネスが両方のEdgeケースを処理するまで終了しないことを確認します(私の乱数の範囲が十分に小さいため、これを回避できると想定しています)。そして、これを文書化します。そうすることで、アルゴリズムを改善しようとする誰もが、それが重大な変更であることを知っているでしょう。
最後の1つis単体テストを作成するもの。このアルゴリズムの実装を破壊するコードには何も侵入していないことを知っておく必要があります。そのときに最初に得られる兆候は、テストが失敗することです。次に、コードに戻って、誰かが何かを修正していて、代わりにそれを壊したと誰かが思ったことを見つけます。誰かがdidアルゴリズムを修正した場合、このテストも修正する必要があります。
結果を確定的にするためにシステムを制御する必要はありません。あなたは正しいアプローチにいます。関数の出力について何が重要であるかを決定し、それをテストします。この場合、結果が40日の範囲内であることが重要であり、それをテストしています。また、常に同じ結果が返されるとは限らないことも重要なので、テストしてみてください。より洗練されたい場合は、結果がなんらかのランダム性テストに合格することをテストできます。
通常、私は提案されたアプローチを正確に使用します。ランダムジェネレーターを制御します。デフォルトのシードでテスト用に初期化する(または、テストケースに適合する数値を返すプロキシで彼を置き換える)ので、決定論的/テスト可能な動作になります。
乱数の品質を(独立性の観点から)確認したい場合は、いくつかの方法があります。 1つの良い方法は カイ二乗検定 です。
関数がランダムな日付を作成する方法によっては、不正な日付(不可能なうるう年、30日の月の31日など)を確認することもできます。
確かに、固定シードの乱数ジェネレータを使用しても問題なく動作しますが、それでも、予測できないものをテストしようとしているだけです。大丈夫です。これは、多数の固定テストを行うことと同じです。ただし、重要なことをテストしてください。ただし、すべてをテストしようとしないでください。ランダムテストはすべてをテストする方法の1つであり、効率的(または高速)ではありません。バグにぶつかる前に、非常に多くのランダム化されたテストを実行しなければならない可能性があります。
ここで私が得ようとしているのは、システムで見つかった各バグのテストを記述するだけでよいということです。 Edgeケースをテストして、極端な状況でも関数が実行されていることを確認しますが、実際には、時間をかけすぎたり、ユニットテストの実行速度を遅くしたり、単にプロセッササイクルを無駄にしたりすることなく実行できる最善の方法です。
ランダム関数をオーバーライドすることをお勧めします。私はPHPでユニットテストをしているので、このコードを書きます:
// If we are unit testing, then...
if (defined('UNIT_TESTING') && UNIT_TESTING)
{
// ...make our my_Rand() function deterministic to aid testing.
function my_Rand($min, $max)
{
return $GLOBALS['random_table'][$min][$max];
}
}
else
{
// ...else make our my_Rand() function truly random.
function my_Rand($min = 0, $max = PHP_INT_MAX)
{
if ($max === PHP_INT_MAX)
{
$max = getrandmax();
}
return Rand($min, $max);
}
}
次に、テストごとに必要なため、random_tableを設定します。
ランダム関数の真のランダム性をテストすることは、完全に別のテストです。私は単体テストでランダム性をテストするのを避け、代わりに個別のテストを行って、使用しているプログラミング言語でランダム関数の真のランダム性をググります。非決定的テスト(存在する場合)は、単体テストから除外する必要があります。おそらく、これらのテストのための個別のスイートがあるかもしれません。これは、実際に合格する失敗の可能性を最小限に抑えるために、人間の入力またははるかに長い実行時間を必要とします。
ユニットテストはこのためのものではないと思います。確率的な値を返す関数にはユニットテストを使用できますが、固定シードを使用できます。その場合、確率論的ではない方法で、いわば、ランダムシードの場合、ユニットテストはあなたが望むものではないと思います。たとえば、 RNGの場合は、システムテストであり、RNGを何度も実行して、RNGの分布または瞬間を確認します。