web-dev-qa-db-ja.com

不確定な出力を伴う単体テスト方法

私もランダムな長さのランダムなパスワードを生成するためのクラスを持っていますが、定義された最小長と最大長の間に制限されています。

私はユニットテストを作成していますが、このクラスで興味深い小さな障害に遭遇しました。ユニットテストの背後にある全体的なアイデアは、それが再現可能でなければならないということです。テストを100回実行すると、同じ結果が100回得られるはずです。そこにある場合もない場合もある、または期待する初期状態にある場合もない場合もあるリソースに依存している場合は、問題のリソースをモックして、テストが常に繰り返し可能であることを確認する必要があります。

しかし、SUTが不確定な出力を生成することになっている場合はどうでしょうか?

最小長と最大長を同じ値に固定すると、生成されたパスワードが予想される長さであることを簡単に確認できます。しかし、許容できる長さの範囲(15から20文字など)を指定すると、テストを100回実行して100パスが得られるという問題がありますが、101回目の実行では9文字の文字列が返される可能性があります。

根本的にかなり単純なパスワードクラスの場合、大きな問題になることはありません。しかし、それは私に一般的なケースについて考えさせました。設計によって不確定な出力を生成しているSUTを処理する場合、通常、最善の方法として受け入れられている戦略は何ですか?

37
GordonM

「非確定的」な出力には、ユニットテストの目的で確定的になる方法が必要です。ランダム性を処理する1つの方法は、ランダムエンジンの置き換えを許可することです。次に例を示します(PHP 5.3以降)。

function DoSomethingRandom($getRandomIntLessThan)
{
    if ($getRandomIntLessThan(2) == 0)
    {
        // Do action 1
    }
    else
    {
        // Do action 2
    }
}

// For testing purposes, always return 1
$alwaysReturnsOne = function($n) { return 1; };
DoSomethingRandom($alwaysReturnsOne);

テストが完全に反復可能であることを確認したい任意の数列を返す関数の特殊なテストバージョンを作成できます。実際のプログラムでは、オーバーライドしないとフォールバックになる可能性があるデフォルトの実装を使用できます。

20
bobbymcr

実際の出力パスワードは、メソッドが実行されるたびに確定されるとは限りませんが、最小長、確定文字セットに含まれる文字など、テスト可能な確定機能がまだあります。

また、パスワードジェネレーターに毎回同じ値をシードすることにより、ルーチンが毎回明確な結果を返すことをテストすることもできます。

21
Mark Baker

「契約」に対するテスト。メソッドが「a〜zで15〜20文字の長さのパスワードを生成する」と定義されている場合は、この方法でテストします。

$this->assertTrue ((bool) preg_match('^[a-z]{15,20}$', $password));

さらに、世代を抽出できるため、それに依存するすべてのものが、別の「静的」ジェネレータークラスを使用してテストできます。

class RandomGenerator implements PasswordGenerator {
  public function create() {
    // Create $rndPwd
    return $rndPwd;
  }
}

class StaticGenerator implements PasswordGenerator {
  private $pwd;
  public function __construct ($pwd) { $this->pwd = $pwd; }
  public function create      ()     { return $this->pwd; }
}
14
KingCrunch

_Password generator_があり、ランダムソースが必要です。

質問で述べたように、randomglobal stateであるので、非決定的な出力を作成します。つまり、システム外の何かにアクセスして値を生成します。

allクラスの場合、そのようなものを取り除くことはできませんが、ランダムな値を作成するためにパスワード生成を分離できます。

_<?php
class PasswordGenerator {

    public function __construct(RandomSource $randomSource) {
        $this->randomSource = $randomSource
    }

    public function generatePassword() {
        $password = '';
        for($length = Rand(10, 16); $length; $length--) {
            $password .= $this-toChar($this->randomSource->Rand(1,26));
        }
    }

}
_

このようにコードを構成すると、テストのためにRandomSourceをモックアウトできます。

RandomSourceを100%テストすることはできませんが、この質問の値をテストするために得た提案はそれに適用できます(Rand->(1,26);が常に1からの数を返すことをテストするのと同様に26に。

6
edorian

次の2つの理由により、受け入れられた回答に同意する必要があります

  1. オーバーフィッティング
  2. 非実用性

(それが多くの状況では良い答えになるかもしれませんが、すべてではなく、ほとんどの場合ではないかもしれないことに注意してください。

それでどういう意味ですか? overfitting とは、統計テストの典型的な問題を意味します。過度に制約されたデータセットに対して確率的アルゴリズムをテストすると、オーバーフィットが発生します。次にアルゴリズムに戻ってアルゴリズムを調整すると、暗黙的にトレーニングデータに非常によく適合します(誤ってfitアルゴリズムをテストデータに適合させます)が、他のすべてのデータはたぶんまったくそうではないかもしれません(テストしないからです)。

(ちなみに、これは常にユニットテストに潜む問題です。これが、優れたテストがcomplete、または少なくともrepresentativeである理由です。与えられたユニットのために、これは一般的に難しいです。

乱数ジェネレータをプラグ可能にしてテストを確定的にする場合は、常に同じ非常に小さい(通常は)非代表的なデータセットに対してテストします。これはデータを歪め、関数のバイアスにつながる可能性があります。

2番目のポイント、非実用性は、確率変数を制御できない場合に発生します。これは通常、乱数ジェネレーターでは発生しません(「本当の」乱数のソースが必要でない限り)が、確率論が他の方法で問題に潜入した場合に発生する可能性があります。たとえば、同時実行コードをテストする場合:競合状態は常に確率的ですが、できない(簡単に)それらを決定論的にすることができます。

これらのケースで信頼を高める唯一の方法は、テストロットすることです。泡立て、すすぎ、繰り返します。これにより、ある程度の信頼性が得られます(その時点で、追加のテスト実行のトレードオフは無視できる程度になります)。

3
Konrad Rudolph

素粒子物理学モンテカルロの場合、私はa事前設定されたランダムシードで非決定的ルーチンを呼び出す「ユニットテスト」{*}を書きました=、次に統計回数を実行し、制約違反(入力エネルギーを超えるエネルギーレベルにアクセスできない、すべてのパスが何らかのレベルを選択する必要があるなど)と以前に記録された結果に対する回帰をチェックします。


{*}このようなテストは、単体テストの「テストを高速化する」原則に違反しているため、受け入れテストや回帰テストなど、他の方法でそれらを特徴付ける方が良いと感じるかもしれません。それでも、ユニットテストフレームワークを使用しました。

依存関係を解消するためにコードをリファクタリングすると、多くのユニットテストの問題は簡単になります。データベース、ファイルシステム、ユーザー、または場合によってはランダム性のソース。

別の見方をすると、ユニットテストは「このコードは私が意図したとおりに機能するのか」という質問に答えるはずです。あなたの場合、コードは非決定的であるため、コードが何をするつもりなのかわかりません。

このことを念頭に置いて、ロジックを小さな、簡単に理解でき、簡単にテストできる分離部分に分割します。具体的には、ランダムソースを入力として受け取り、パスワードを出力として生成する個別のメソッド(またはクラス!)を作成します。そのコードは明らかに確定的です。

単体テストでは、毎回同じ非ランダム入力にフィードします。非常に小さなランダムストリームの場合は、テストの値をハードコードするだけです。それ以外の場合は、テストでRNGに一定のシードを提供します。

より高いレベルのテスト(「受け入れ」や「統合」などと呼ばれます)では、真のランダムソースでコードを実行できます。

2
Jay Bazuzi

ここには実際に複数の責任があります。ユニットテスト、特にTDDは、この種のことを強調するのに最適です。

責任は次のとおりです。

1)乱数ジェネレータ。 2)パスワードフォーマッター。

パスワードフォーマッタは、乱数ジェネレータを使用します。インターフェイスとしてコンストラクターを介してジェネレーターをフォーマッターに挿入します。これで、乱数ジェネレーターを完全にテストでき(統計テスト)、模擬乱数ジェネレーターを注入してフォーマッターをテストできます。

より良いコードを取得できるだけでなく、より良いテストを取得できます。

2
Rob Smyth

他の人がすでに述べたように、あなたはnit testこのコードをランダム性を取り除いてください。

また、乱数ジェネレータをそのままにして、コントラクト(パスワードの長さ、許可された文字など)のみをテストし、失敗した場合はシステムを再現するのに十分な情報をダンプする、より高レベルのテストが必要になる場合もあります。ランダムテストが失敗した1つのインスタンスの状態。

テスト自体が再現性がないことは問題ではありません-これが一度失敗した理由を見つけることができる限り。

2
Simon Richter

上記の回答のほとんどは、乱数ジェネレーターをモックする方法が適していることを示していますが、私は単に組み込みのmt_Rand関数を使用していました。モックを許可するということは、作成時に乱数ジェネレータを注入する必要があるようにクラスを書き直すことを意味します。

またはそう思いました!

名前空間の追加の結果の1つは、組み込みのモッキングPHP関数が信じられないほど単純なものから非常に単純なものになりました。SUTが特定の名前空間にある場合、実行する必要があるのは定義することだけです。その名前空間での単体テストでの独自のmt_Rand関数。テスト期間中、組み込みのPHP関数の代わりに使用されます。

これが最終的なテストスイートです。

namespace gordian\reefknot\util;

/**
 * The following function will take the place of mt_Rand for the duration of 
 * the test.  It always returns the number exactly half way between the min 
 * and the max.
 */
function mt_Rand ($min = 42, $max = NULL)
{
    $min    = intval ($min);
    $max    = intval ($max);

    $max    = $max < $min? $min: $max;
    $ret    = round (($max - $min) / 2) + $min;

    //fwrite (STDOUT, PHP_EOL . PHP_EOL . $ret . PHP_EOL . PHP_EOL);
    return ($ret);
}

/**
 * Override the password character pool for the test 
 */
class PasswordSubclass extends Password
{
    const CHARLIST  = 'AAAAAAAAAA';
}

/**
 * Test class for Password.
 * Generated by PHPUnit on 2011-12-17 at 18:10:33.
 */
class PasswordTest extends \PHPUnit_Framework_TestCase
{

    /**
     * @var gordian\reefknot\util\Password
     */
    protected $object;

    const PWMIN = 15;
    const PWMAX = 20;

    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */
    protected function setUp ()
    {
    }

    /**
     * Tears down the fixture, for example, closes a network connection.
     * This method is called after a test is executed.
     */
    protected function tearDown ()
    {

    }

    public function testGetPassword ()
    {
        $this -> object = new PasswordSubclass (self::PWMIN, self::PWMAX);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ((bool) preg_match ('/^A{' . self::PWMIN . ',' . self::PWMAX . '}$/', $pw));
        $this -> assertTrue (strlen ($pw) >= self::PWMIN);
        $this -> assertTrue (strlen ($pw) <= self::PWMAX);
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testGetPasswordFixedLen ()
    {
        $this -> object = new PasswordSubclass (self::PWMIN, self::PWMIN);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ($pw === 'AAAAAAAAAAAAAAA');
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testGetPasswordFixedLen2 ()
    {
        $this -> object = new PasswordSubclass (self::PWMAX, self::PWMAX);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ($pw === 'AAAAAAAAAAAAAAAAAAAA');
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testInvalidLenThrowsException ()
    {
        $exception  = NULL;
        try
        {
            $this -> object = new PasswordSubclass (self::PWMAX, self::PWMIN);
        }
        catch (\Exception $e)
        {
            $exception  = $e;
        }
        $this -> assertTrue ($exception instanceof \InvalidArgumentException);
    }
}

PHP内部関数のオーバーライドは、私には思いもよらなかった名前空間のもう1つの使用法なので、これについて言及したいと思いました。これについて助けてくれたみんなに感謝します。

1
GordonM

この状況に含める必要のある追加のテストがあります。これは、パスワードジェネレーターを繰り返し呼び出して実際に異なるパスワードが生成されることを確認するためのテストです。スレッドセーフなパスワードジェネレータが必要な場合は、複数のスレッドを使用して同時呼び出しをテストする必要もあります。

これは基本的に、ランダムな関数を適切に使用し、すべての呼び出しで再シードしないことを保証します。

0
Torbjørn