web-dev-qa-db-ja.com

本質的にランダム/非決定論的なアルゴリズムのユニットテスト

私の現在のプロジェクトは、簡潔に言えば、「制約のあるランダムなイベント」の作成に関係しています。基本的に検査のスケジュールを作成しています。それらのいくつかは、厳しいスケジュール制約に基づいています。週に1回、金曜日の午前10時に検査を実行します。その他の検査は「ランダム」です。 「検査は週に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;
    }

...そしてこれが結果です:

enter image description here

LSBの拒否(範囲内に収まるまで数値をビットシフトする)は、非常に説明しやすい理由で、ひどいものでした。最大値を下回るまで任意の数を2で除算すると、最大値を下回るとすぐに終了します。重要な範囲については、結果を上位3分の1に偏らせます(ヒストグラムの詳細な結果に見られるように) )。これはまさに私が完成した日から見た行動でした。すべての時間は非常に特定の日の午後でした。

MSB拒否(最上位ビットを一度に1つずつ削除して範囲内になるまで)の方が優れていますが、各ビットで非常に大きな数を切り落とすため、均等に分散されません。上端と下端に数値が表示される可能性は低いため、中央の3分の1に向かってバイアスがかかります。これは、ランダムデータを鐘のような曲線に「正規化」しようとする人にメリットがありますが、2つ以上の小さい乱数(ダイスを投げるのと同様)の合計は、より自然な曲線になります。私の目的のために、それは失敗します。

このテストに合格したのは、モジュロ除算によって制約することだけでしたが、これも3つの中で最速であることが判明しました。 Moduloは、その定義により、使用可能な入力が与えられれば、可能な限り分布を生成します。

42
KeithS

ここで実際にテストしたいのは、ランダマイザーからの特定の結果セットが与えられた場合、残りのメソッドは正しく実行されるということです。

それがあなたが探しているものである場合、ランダマイザーを mock にして、テストの領域内で確定的にします。

私は通常、GUIDジェネレーターおよびDateTime.Nowを含む)あらゆる種類の非決定的または予測不可能な(テスト作成時の)データのモックオブジェクトを持っています。

編集から、コメントから:PRNG(その用語は昨夜、私を免れた))を可能な限り低いレベルでモックする必要があります。つまり、バイト配列を生成した後ではなく、 Int64s。または両方のレベルでも、Int64の配列への変換が意図したとおりに機能することをテストしてから、DateTimesの配列への変換が意図したとおりに機能することを個別にテストできます。Jonathonが言ったように、セットシード、または返すバイトの配列を指定できます。

PRNGのフレームワーク実装が変更されても壊れないので、後者を好みます。しかし、シードを与えることの1つの利点は、本番環境で行わなかったケースを見つけた場合、意図したとおりに機能するため、アレイ全体ではなく、1つの数値をログに記録して、それを複製できるようにする必要があります。

これがすべて言った、あなたはそれが理由のためにPseudo乱数ジェネレータと呼ばれていることを覚えておく必要があります。そのレベルでもsomeバイアスがあるかもしれません。

17
pdr

これは馬鹿げた答えのように聞こえるでしょうが、これは私が以前にそれを行ったことを見た方法なので、私はそこに投げ出します:

コードをPRNG-ランダム化シードをランダム化を使用するすべてのコードに渡します。次に、単一のシード(または、これにより、多数の法則に依存することなく、コードを適切にテストできます。

それは狂ったように聞こえますが、これは軍がそれをする方法です(それか、彼らはまったくランダムではない「ランダムテーブル」を使用します)

23
Jonathan Rich

「ランダムですか(十分)」というのは、信じられないほど微妙な質問です。簡単に言えば、従来の単体テストではうまくいかないということです。一連のランダムな値を生成し、それらをさまざまな統計テストに送信して、ニーズに対して十分にランダムであるという高い信頼を与える必要があります。

パターンがあります-結局、疑似乱数ジェネレータを使用しています。しかし、ある時点では、アプリケーションにとって「十分」なものになります(十分に良いというのは、一端のゲーム間でLOTが異なり、比較的単純なジェネレーターで十分な場合、シーケンスを決定することが実際には実行不可能である暗号法に至るまでです。決定的で装備の整った攻撃者による)。

ウィキペディアの記事 http://en.wikipedia.org/wiki/Randomness_tests とそのフォローアップリンクに詳細情報があります。

6
James Iry

2つの答えがあります。

===最初の回答===

あなたの質問のタイトルを見るやいなや、私は飛び込んで解決策を提案するようになりました。私の解決策は、他のいくつかの提案と同じでした:乱数ジェネレーターをモックアウトすることです。結局のところ、優れた単体テストを作成するためにこのトリックを必要とするいくつかの異なるプログラムを構築し、すべてのコーディングにおいて、乱数へのモック可能なアクセスを標準的な方法にし始めています。

しかし、私はあなたの質問を読みました。そして、あなたが説明する特定の問題については、それは答えではありません。あなたの問題は、乱数を使用するプロセスを予測可能にする必要があるということではありませんでした(したがって、テスト可能です)。むしろ、問題は、アルゴリズムがRNGから均一にランダムな出力をアルゴリズムからの制約内均一出力にマッピングしたことを確認することでした-基礎となるRNGが均一である場合、検査時間が均等に分散されることになります(問題の制約)。

これは本当に難しい(しかしかなり明確な)問題です。これは興味深い問題であることを意味します。すぐにこれを解決するためのいくつかの本当に素晴らしいアイデアを考え始めました。私がホットショットプログラマだったころ、私はこれらのアイデアで何かを始めたかもしれません。しかし、私はもうホットショットプログラマではありません...私は今より経験があり、より熟練していることが好きです。

だから私は難しい問題に飛び込むのではなく、自分自身に考えました。これの価値は何ですか?そして答えはがっかりした。あなたのバグは既に解決されており、あなたは将来この問題に真剣に取り組むでしょう。外部の状況では問題が発生せず、アルゴリズムが変更されるだけです。この興味深い問題に取り組む唯一の理由は、TDD(テスト駆動設計)の実践を満たすためでした。私が学んだことの1つがあるとすれば、それが価値のないときに任意の練習を盲目的に固執することは問題を引き起こすということです。私の提案はこれです:これについてのテストを書かないで、次に進んでください


=== 2番目の回答===

うわー...なんてクールな問題だ!

ここで行う必要があるのは、使用するRNGが均一に分散された数値を生成する場合、検査日時を選択するアルゴリズムが(問題の制約内で)均一に分散された出力を生成することを検証するテストを記述することです。難易度の高い順に並べられたいくつかのアプローチがあります。

  1. あなたはブルートフォースを適用することができます。入力として実際のRNGを使用して、アルゴリズムを完全な回数実行します。出力結果を調べて、それらが均一に分布しているかどうかを確認します。分布が特定のしきい値を超えて完全に均一から変化している場合、テストを失敗させ、しきい値を低く設定できない問題を確実に検出する必要があります。つまり、誤検知(ランダムな偶然によるテストの失敗)の可能性が非常に小さい(中規模のコードベースでは1%未満)ことを確認するには、膨大な数の実行が必要になります。大きなコードベース)。

  2. アルゴリズムを、すべてのRNG出力の連結を入力として受け取り、検査時間を出力として生成する関数と見なしてください。この関数が区分的に連続していることがわかっている場合は、プロパティをテストする方法があります。 RNGをモック可能なRNGに置き換え、アルゴリズムを何度も実行して、均一に分散されたRNG出力を生成します。したがって、コードが2つのRNG呼び出しを必要とし、それぞれが[0..1]の範囲にある場合、テストでアルゴリズムを100回実行して、値[(0.0,0.0)、(0.0,0.1)、(0.0、 0.2)、...(0.0、0.9)、(0.1、0.0)、(0.1、0.1)、...(0.9、0.9)]。次に、100回の実行の出力が(ほぼ)許容範囲内で均一に分布しているかどうかを確認できます。

  3. 本当に信頼できる方法でアルゴリズムを検証する必要があり、アルゴリズムについて仮定を立てることができない場合OR何度も実行しても、問題を攻撃できますが、アルゴリズムのプログラミング方法にいくつかの制約が必要です。例として PyPy とそれらの Object Space アプローチを確認してください。実際にアルゴリズムを実行する代わりに、出力分布の形状を計算するだけです(RNG入力が均一であると想定)。もちろん、これには、そのようなツールを構築し、アルゴリズムを組み込む必要があります。 PyPyまたはその他のツールで、コンパイラーに大幅な変更を加え、それを使用してコードを分析するのが簡単です。

5
mcherm

Sevcikova et al:「確率システムの自動テスト:統計的に根拠のあるアプローチ」( [〜#〜] pdf [〜#〜] )をご覧ください。

この方法論は rbanSim シミュレーションプラットフォームのさまざまなテストケースに実装されています。

3
krlmlr

単体テストの場合、ランダムジェネレーターを予測可能な結果を​​生成するクラスに置き換えますすべてのコーナーケースをカバーする。つまり疑似ランダマイザーが可能な最低値と最高可能値を生成し、同じ結果が連続して数回生成されることを確認してください。

単体テストで見落とされたくないRandom.nextInt(1000)が0または999を返すと、1つずれたバグが発生します。

3
user281377

単純なヒストグラムアプローチは優れた最初のステップですが、ランダム性を証明するには不十分です。均一なPRNG=の場合、(少なくとも)2次元散布図も生成します(xは以前の値、yは新しい値です)。このプロットも均一でなければなりません。システムには意図的な非線形性があるため、これは状況によっては複雑になります。

私のアプローチは:

  1. ソースPRNG=が十分にランダムであることを標準の統計的測定値を使用して)検証(または指定されたものと見なします)
  2. 制約のないPRNGからdatetimeへの変換が出力空間全体で十分にランダムであることを確認します(これにより、変換に偏りがないことを確認します)。ここでは、単純な1次均一性テストで十分です。
  3. 制約されたケースが十分に均一であることを確認します(有効なビンに対する単純な1次の均一性テスト)。

これらの各テストは統計的であり、高い信頼度で偽陽性と偽陰性を回避するために多数のサンプルポイントが必要です。

変換/制約アルゴリズムの性質について:

Given:擬似ランダム値を生成するメソッドpwhere 0 <=p<=[〜#〜] m [〜#〜]

必要:出力y(おそらく不連続)範囲0 <=y<=[〜#〜] n [〜#〜]<=[ 〜#〜] m [〜#〜]

アルゴリズム:

  1. r = floor(M / N)、つまり、入力範囲内に収まる完全な出力範囲の数を計算します。
  2. pの最大許容値を計算します:p_max = r * N
  3. p_max以下の値が見つかるまで、pの値を生成します
  4. y = p / rを計算する
  5. yが受け入れられる場合はそれを返し、そうでない場合はステップ3を繰り返します。

重要なのは、不均一に折り畳むのではなく、許容できない値を破棄することです。

擬似コードで:

# assume prng generates non-negative values
def randomInRange(min, max, prng):
    range = max - min
    factor = prng.max / range

    do:
        value = prng()
    while value > range * factor
    return (value / factor) + min

def constrainedRandom(constraint, prng):
    do:
        value = randomInRange(constraint.min, constraint.max, prng)
    while not constraint.is_acceptable(value)
2
Frank Szczerba

コードが失敗しないこと、または適切な場所に適切な例外をスローすることの検証とは別に、有効な入力/応答のペアを作成し(これを手動で計算することもできます)、テストに入力を送り、期待される応答を返すことを確認します。すばらしいとは言えませんが、あなたができることはそれだけです。ただし、実際にはランダムではありません。スケジュールを作成したら、ルールの適合性をテストできます。週に3〜9回の検査が必要です。検査が行われた正確な時間をテストする実際の必要性や能力はありません。

1
Evgeni

これを何度も実行して、必要なディストリビューションが得られるかどうかを確認するよりもよい方法はありません。 50の潜在的な検査スケジュールが許可されている場合は、テストを500回実行し、各スケジュールが10回近く使用されることを確認します。ランダムジェネレーターシードを制御して確定性を高めることができますが、これにより、テストが実装の詳細とより密接に結合されます。

1
Karl Bielefeldt

具体的な定義がない曖昧な状態をテストすることはできません。生成された日付がすべてのテストに合格した場合、理論的にはアプリケーションは正しく機能しています。コンピュータは、そのようなテストの基準を認めることができないため、日付が「ランダム」であるかどうかを通知できません。すべてのテストに合格したが、アプリケーションの動作がまだ適切でない場合、テストカバレッジは(TDDの観点から)経験的に不十分です。

私の考えでは、最善の方法は、分布が人間のにおいテストに合格するように、いくつかの任意の日付生成制約を実装することです。

1
leo

このケースはプロパティベースのテストに最適です。

簡単に言えば、これはテストのモードであり、テストフレームワークがテスト中のコードの入力を生成し、アサーションが出力の検証propertiesを検証します。その後、フレームワークは、テスト中のコードを「攻撃」し、それをエラーに追い込むのに十分なほど賢くなります。フレームワークは通常、乱数ジェネレータシードをハイジャックするのにも十分スマートです。通常、フレームワークは最大でN個のテストケースを生成するか、最大でN秒実行するように構成でき、最後の実行からの失敗したテストケースを記憶し、新しいコードバージョンに対して最初に再実行します。これにより、開発中の高速な反復サイクルと、帯域外/ CIでの低速で包括的なテストが可能になります。

sum関数をテストする(ダム、失敗)の例を次に示します。

_@given(lists(floats()))
def test_sum(alist):
    result = sum(alist)
    assert isinstance(result, float)
    assert result > 0
_
  • テストフレームワークは一度に1つのリストを生成します
  • リストの内容は浮動小数点数になります
  • sumが呼び出され、結果のプロパティが検証されます
  • 結果は常に浮動です
  • 結果は肯定的です

このテストでは、sumに多数の「バグ」が見つかります(これらの all を自分で推測できた場合のコメント):

  • sum([]) is 0(int、floatではない)
  • sum([-0.9])は負です
  • sum([0.0])は厳密には正ではありません
  • sum([..., nan]) is nanは正ではありません

デフォルト設定では、hpythesisは1つの「不良」入力が見つかった後にテストを中止します。これはTDDに適しています。多くの/すべての「悪い」入力を報告するように設定することは可能だと思っていましたが、今はそのオプションを見つけることができません。

OPの場合、検証されるプロパティはより複雑になります。検査タイプAの存在、検査タイプAは週3回、検査時間Bは常に午後12時、検査タイプCは9から9 [指定されたスケジュールは1週間]検査タイプA、B、Cすべて存在、など.

最もよく知られているライブラリは、HaskellのQuickCheckです。他の言語でのそのようなライブラリのリストについては、以下のWikipediaのページを参照してください。

https://en.wikipedia.org/wiki/QuickCheck

Hypothesis(Pythonの場合)は、この種のテストについて適切に記述しています。

https://hypothesis.works/articles/what-is-property-based-testing/

0
Dima Tisnek

ランダマイザーの出力を記録するだけです(疑似または量子/カオスまたは実世界)。次に、単体テストケースを作成するときに、テスト要件に適合する、または潜在的な問題やバグを明らかにする「ランダム」シーケンスを保存して再生します。

0
hotpaw2