Metode pengujian unit dengan output tidak pasti

37

Saya memiliki kelas yang dimaksudkan untuk menghasilkan kata sandi acak dengan panjang yang juga acak, tetapi terbatas antara min yang ditentukan dan panjang maks.

Saya sedang membangun unit test, dan mengalami hambatan kecil yang menarik dengan kelas ini. Seluruh ide di balik unit test adalah harus diulang. Jika Anda menjalankan tes seratus kali, itu akan memberikan hasil yang sama seratus kali. Jika Anda bergantung pada beberapa sumber daya yang mungkin ada atau tidak ada di sana atau mungkin atau mungkin tidak dalam kondisi awal yang Anda harapkan, maka Anda harus mengejek sumber daya yang dimaksud untuk memastikan bahwa pengujian Anda benar-benar selalu dapat diulang.

Tetapi bagaimana dalam kasus-kasus di mana SUT seharusnya menghasilkan keluaran yang tidak ditentukan?

Jika saya memperbaiki panjang min dan maks ke nilai yang sama maka saya dapat dengan mudah memeriksa bahwa kata sandi yang dihasilkan memiliki panjang yang diharapkan. Tetapi jika saya menentukan rentang panjang yang dapat diterima (katakanlah 15 - 20 karakter), maka Anda sekarang memiliki masalah bahwa Anda dapat menjalankan tes seratus kali dan mendapatkan 100 lintasan tetapi pada lari ke-101 Anda mungkin mendapatkan string 9 karakter kembali.

Dalam hal kelas kata sandi, yang intinya cukup sederhana, seharusnya tidak membuktikan masalah besar. Tapi itu membuat saya berpikir tentang kasus umum. Apa strategi yang biasanya diterima sebagai yang terbaik untuk diambil ketika berhadapan dengan SUT yang menghasilkan output yang tidak ditentukan oleh desain?

GordonM
sumber
9
Mengapa memilih dekat? Saya pikir ini pertanyaan yang sangat valid.
Mark Baker
Hah, terima kasih atas komentarnya. Bahkan tidak memperhatikan itu, tapi sekarang aku bertanya-tanya hal yang sama. Satu-satunya hal yang dapat saya pikirkan adalah tentang kasus umum dan bukan kasus tertentu, tetapi saya dapat memposting sumber untuk kelas kata sandi yang disebutkan di atas dan bertanya, "Bagaimana cara menguji kelas itu?" alih-alih "Bagaimana cara menguji kelas yang tidak ditentukan?"
GordonM
1
@MarkBaker Karena sebagian besar pertanyaan yang belum diputuskan ada di programmers.se. Ini suara untuk migrasi, bukan untuk menutup pertanyaan.
Ikke

Jawaban:

20

Output "Non-deterministik" harus memiliki cara menjadi deterministik untuk keperluan pengujian unit. Salah satu cara untuk menangani keacakan adalah dengan memungkinkan penggantian mesin acak. Berikut ini sebuah contoh (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);

Anda dapat membuat versi uji khusus dari fungsi yang mengembalikan urutan angka apa pun yang Anda ingin memastikan pengujian sepenuhnya dapat diulang. Dalam program nyata, Anda dapat memiliki implementasi default yang bisa menjadi mundur jika tidak diganti.

bobbymcr
sumber
1
Semua jawaban yang diberikan memiliki saran bagus yang saya gunakan, tetapi ini adalah salah satu yang menurut saya menjadi inti permasalahan sehingga mendapat penerimaan.
GordonM
1
Cukup banyak paku di kepala. Sementara non-deterministik, masih ada batasan.
surfasb
21

Kata sandi keluaran aktual mungkin tidak ditentukan setiap kali metode dijalankan, tetapi masih akan memiliki fitur menentukan yang dapat diuji, seperti panjang minimum, karakter yang termasuk dalam rangkaian karakter yang ditentukan, dll.

Anda juga dapat menguji apakah rutin mengembalikan hasil yang pasti setiap kali dengan seeding generator kata sandi Anda dengan nilai yang sama setiap kali.

Mark Baker
sumber
Kelas PW mempertahankan konstanta yang pada dasarnya merupakan kumpulan karakter kata sandi yang harus dihasilkan. Dengan mensubklasifikasikan dan mengganti konstanta dengan satu karakter, saya berhasil menghilangkan satu area non-determinasi untuk keperluan pengujian. Jadi terima kasih.
GordonM
14

Uji terhadap "kontrak". Ketika metode didefinisikan sebagai "menghasilkan kata sandi dengan panjang 15 hingga 20 karakter dengan az", ujilah dengan cara ini

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

Tambahan Anda dapat mengekstrak generasi, jadi semuanya, yang bergantung padanya, dapat diuji menggunakan kelas generator "statis" lainnya

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; }
}
KingCrunch
sumber
Regex yang Anda berikan terbukti bermanfaat sehingga saya menyertakan versi tweak dalam pengujian saya. Terima kasih.
GordonM
6

Anda memiliki Password generatordan Anda membutuhkan sumber acak.

Seperti yang Anda nyatakan dalam pertanyaan a randommembuat keluaran non-deterministik karena merupakan keadaan global . Berarti itu mengakses sesuatu di luar sistem untuk menghasilkan nilai.

Anda tidak pernah bisa menghilangkan sesuatu seperti itu untuk semua kelas Anda, tetapi Anda dapat memisahkan pembuatan kata sandi untuk pembuatan nilai acak.

<?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));
        }
    }

}

Jika Anda menyusun kode seperti ini, Anda dapat mengejek RandomSourcetes Anda.

Anda tidak akan dapat menguji 100% RandomSourcetetapi saran yang Anda dapatkan untuk menguji nilai-nilai dalam pertanyaan ini dapat diterapkan untuk itu (Seperti pengujian yang rand->(1,26);selalu mengembalikan angka dari 1 hingga 26.

pendidik
sumber
Itu jawaban yang bagus.
Nick Hodges
3

Dalam kasus fisika partikel Monte Carlo, saya telah menulis "unit test" {*} yang memanggil rutin non-deterministik dengan seed acak yang telah ditetapkan , dan kemudian menjalankan sejumlah statistik kali dan memeriksa pelanggaran kendala (level energi) di atas energi input harus tidak dapat diakses, semua lintasan harus memilih beberapa level, dll) dan regresi terhadap hasil yang direkam sebelumnya.


{*} Tes semacam itu melanggar prinsip "buat tes cepat" untuk pengujian unit, jadi Anda mungkin merasa lebih baik mengkarakterisasinya dengan beberapa cara lain: tes penerimaan atau tes regresi, misalnya. Namun, saya menggunakan kerangka pengujian unit saya.

dmckee
sumber
3

Saya harus tidak setuju dengan jawaban yang diterima , karena dua alasan:

  1. Overfitting
  2. Ketidakpraktisan

(Perhatikan bahwa itu mungkin jawaban yang baik dalam banyak situasi, tetapi tidak dalam semua, dan mungkin tidak dalam kebanyakan.)

Jadi apa yang saya maksud dengan itu? Nah, dengan overfitting yang saya maksud adalah masalah khas pengujian statistik: overfitting terjadi ketika Anda menguji algoritma stokastik terhadap sekumpulan data yang terlalu terbatas. Jika kemudian Anda kembali dan memperbaiki algoritme Anda, Anda secara implisit akan membuatnya cocok dengan data pelatihan dengan sangat baik (Anda secara tidak sengaja menyesuaikan algoritme Anda dengan data uji), tetapi semua data lain mungkin tidak sama sekali (karena Anda tidak pernah menguji hal itu) .

(Kebetulan, ini selalu merupakan masalah yang mengintai pengujian unit. Inilah sebabnya mengapa tes yang baik selesai , atau setidaknya mewakili untuk unit yang diberikan, dan ini sulit secara umum.)

Jika Anda membuat pengujian Anda deterministik dengan membuat generator angka acak yang dapat dicolokkan, Anda selalu menguji terhadap kumpulan data yang sangat kecil dan (biasanya) tidak representatif . Ini memiringkan data Anda dan dapat menyebabkan bias dalam fungsi Anda.

Poin kedua, ketidakpraktisan, muncul ketika Anda tidak memiliki kendali atas variabel stokastik. Ini biasanya tidak terjadi dengan generator bilangan acak (kecuali jika Anda memerlukan sumber acak "nyata") tetapi itu bisa terjadi ketika stokastik menyelinap ke dalam masalah Anda dengan cara lain. Misalnya, ketika menguji kode bersamaan: kondisi balapan selalu stokastik, Anda tidak dapat (dengan mudah) membuatnya menjadi deterministik.

Satu-satunya cara untuk meningkatkan kepercayaan diri dalam kasus-kasus itu adalah dengan banyak tes . Busa, bilas, ulangi. Ini meningkatkan kepercayaan diri, hingga tingkat tertentu (di mana trade-off untuk uji coba tambahan menjadi diabaikan).

Konrad Rudolph
sumber
2

Anda sebenarnya memiliki banyak tanggung jawab di sini. Pengujian unit dan khususnya TDD sangat bagus untuk menyoroti hal semacam ini.

Tanggung jawabnya adalah:

1) pembangkit bilangan acak. 2) Pemformat kata sandi.

Pemformat kata sandi menggunakan penghasil angka acak. Suntikkan generator ke formatter Anda melalui konstruktornya sebagai antarmuka. Sekarang Anda dapat sepenuhnya menguji generator nomor acak Anda (uji statistik) dan Anda dapat menguji formatter dengan menyuntikkan generator nomor acak yang diejek.

Anda tidak hanya mendapatkan kode yang lebih baik, Anda juga mendapatkan tes yang lebih baik.

Rob Smyth
sumber
2

Seperti yang lain telah disebutkan, Anda menguji kode ini dengan menghapus keacakan.

Anda mungkin juga ingin memiliki tes tingkat lebih tinggi yang membiarkan generator nomor acak tetap di tempatnya, menguji hanya kontrak (panjang kata sandi, karakter yang diizinkan, ...) dan, jika gagal, membuang informasi yang cukup untuk memungkinkan Anda mereproduksi sistem menyatakan dalam satu contoh di mana tes acak gagal.

Tidak masalah bahwa tes itu sendiri tidak dapat diulang - selama Anda dapat menemukan alasan mengapa tes gagal kali ini.

Simon Richter
sumber
2

Banyak kesulitan pengujian unit menjadi sepele ketika Anda refactor kode Anda untuk memutuskan dependensi. Basis data, sistem file, pengguna, atau dalam kasus Anda, sumber keacakan.

Cara lain untuk melihat adalah bahwa unit test seharusnya menjawab pertanyaan "apakah kode ini melakukan apa yang saya inginkan?". Dalam kasus Anda, Anda tidak tahu apa yang ingin Anda lakukan kode karena itu non-deterministik.

Dengan pikiran ini, pisahkan logika Anda menjadi bagian-bagian kecil, mudah dipahami, dan mudah diuji. Khususnya, Anda membuat metode yang berbeda (atau kelas!) Yang mengambil sumber keacakan sebagai inputnya, dan menghasilkan kata sandi sebagai output. Kode itu jelas deterministik.

Dalam pengujian unit Anda, Anda memberinya input tidak-cukup-acak yang sama setiap kali. Untuk aliran acak yang sangat kecil, cukup hard-code nilai-nilai dalam pengujian Anda. Jika tidak, berikan benih konstan ke RNG dalam pengujian Anda.

Pada tingkat pengujian yang lebih tinggi (menyebutnya "penerimaan" atau "integrasi" atau apa pun), Anda akan membiarkan kode berjalan dengan sumber acak yang sebenarnya.

Jay Bazuzi
sumber
Jawaban ini cocok untuk saya: Saya benar-benar memiliki dua fungsi dalam satu: generator nomor acak, dan fungsi yang melakukan sesuatu dengan nomor acak itu. Saya cukup refactored, dan sekarang dapat dengan mudah menguji bagian nondeterministic dari kode, dan memberi makan parameter yang dihasilkan oleh bagian acak. Yang menyenangkan adalah saya kemudian dapat memberinya (set berbeda) parameter tetap dalam pengujian unit saya (saya menggunakan generator nomor acak dari perpustakaan standar, jadi bukan pengujian unit itu pula).
neuronet
1

Sebagian besar jawaban di atas menunjukkan bahwa mengejek generator nomor acak adalah cara untuk pergi, namun saya hanya menggunakan fungsi mt_rand bawaan. Mengizinkan mengejek berarti menulis ulang kelas untuk meminta generator nomor acak disuntikkan pada waktu konstruksi.

Atau begitulah yang saya pikirkan!

Salah satu konsekuensi dari penambahan ruang nama adalah bahwa mengejek yang dibangun dalam fungsi PHP telah berubah dari sangat sulit menjadi sangat sederhana. Jika SUT berada dalam namespace yang diberikan maka yang perlu Anda lakukan adalah mendefinisikan fungsi mt_rand Anda sendiri dalam tes unit di bawah namespace itu, dan itu akan digunakan sebagai ganti fungsi built in PHP selama durasi tes.

Berikut ini adalah rangkaian uji yang diselesaikan:

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);
    }
}

Saya pikir saya akan menyebutkan ini, karena mengesampingkan fungsi internal PHP adalah penggunaan lain untuk ruang nama yang tidak terpikir oleh saya. Terima kasih kepada semua orang untuk bantuannya.

GordonM
sumber
0

Ada tes tambahan yang harus Anda sertakan dalam situasi ini, dan itu adalah satu untuk memastikan bahwa panggilan berulang ke pembuat kata sandi benar-benar menghasilkan kata sandi yang berbeda. Jika Anda memerlukan pembuat kata sandi yang aman, Anda juga harus menguji panggilan simultan menggunakan beberapa utas.

Ini pada dasarnya memastikan bahwa Anda menggunakan fungsi acak Anda dengan benar, dan tidak melakukan seeding ulang pada setiap panggilan.

Torbjørn
sumber
Sebenarnya, kelas dirancang sedemikian rupa sehingga kata sandi dihasilkan pada panggilan pertama ke getPassword () dan kemudian kait, sehingga selalu mengembalikan kata sandi yang sama untuk masa pakai objek. Rangkaian pengujian saya sudah memeriksa bahwa beberapa panggilan ke getPassword () pada instance kata sandi yang sama selalu mengembalikan string kata sandi yang sama. Adapun keamanan thread, itu tidak benar-benar menjadi perhatian dalam PHP :)
GordonM