Belirsiz çıkışlı birim test yöntemleri


37

Aynı zamanda rastgele fakat tanımlanmış bir minimum ve maksimum uzunluk arasında olmak üzere sınırlı bir uzunluktaki rasgele bir şifre oluşturması gereken bir sınıfa sahibim.

Birim testleri yapıyorum ve bu sınıfa ilginç küçük bir engelle girdim. Birim testinin arkasındaki bütün fikir, tekrarlanabilir olması gerektiğidir. Testi yüzlerce kez uygularsanız, aynı sonuçları yüzlerce kez vermelidir. Orada olabileceğiniz veya olamayacağınız ya da olamayacağınız ya da olamayacağınız bazı kaynaklara bağlıysanız, beklediğiniz başlangıç ​​durumunda olabilir ya da olmayabilir, o zaman testinizin gerçekten tekrarlanabilir olmasını sağlamak için söz konusu kaynağı alay etmek zorundasınız.

Peki ya SUT'un belirsiz çıktılar üretmesi gereken durumlarda?

Min ve maksimum uzunluğu aynı değere sabitlerseniz, oluşturulan parolanın beklenen uzunlukta olup olmadığını kolayca kontrol edebilirim. Ancak, kabul edilebilir bir dizi uzunluk belirtirsem (15 - 20 karakter), o zaman şimdi testi yüzlerce defa çalıştırıp 100 geçiş elde etmekte sorun yaşarsınız;

Özünde oldukça basit olan bir şifre sınıfı söz konusu olduğunda, büyük bir problemi kanıtlamamalıdır. Ama bu beni genel dava hakkında düşündürdü. Tasarımla belirsiz çıktılar üreten SUT'larla uğraşırken genellikle alınması en iyi strateji olarak kabul edilen strateji nedir?


9
Neden yakın oyla? Bence bu tamamen geçerli bir soru.
Mark Baker,

Huh, yorumunuz için teşekkürler. Bunu farketmedim bile, ama şimdi aynı şeyi merak ediyorum. Aklıma gelen tek şey, bunun spesifik bir davadan ziyade genel bir dava ile ilgili olduğu, ancak yukarıda belirtilen parola sınıfının kaynağını gönderebilir ve “Bu sınıfı nasıl test ederim?” Diye sorabilirim. "Belirsiz bir sınıfı nasıl test ederim?"
GordonM

1
@MarkBaker En ilginç soruların çoğu programmers.se'dadır. Soruyu kapatmak değil, göç için bir oy.
Belirtilmemiş

Yanıtlar:


20

“Deterministik olmayan” çıktı, birim testi amacıyla deterministik hale gelme yoluna sahip olmalıdır. Rasgele olmanın bir yolu rasgele motorun değiştirilmesine izin vermektir. İşte bir örnek (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);

Testin tamamen tekrarlanabilir olduğundan emin olmak istediğiniz herhangi bir sayı sırasını döndüren özel bir test testi yapabilirsiniz. Gerçek programda, geçersiz kılınmazsa geri dönüş olabilecek varsayılan bir uygulamaya sahip olabilirsiniz.


1
Verilen tüm cevapların benim kullandığım iyi önerileri vardı, fakat benim temel meseleyi çektiğimi düşündüğüm bir cevap bu.
GordonM

1
Hemen hemen kafasına çiviler. Deterministik olmamakla birlikte, hala sınırlar var.
surfasb

21

Gerçek çıkış şifresi, metodun her çalıştırılışında tespit edilemeyebilir, ancak minimum uzunluk, belirli bir karakter kümesine giren karakterler vb. Gibi test edilebilecek özelliklere sahip olacaktır.

Ayrıca, şifre oluşturucunuzu her seferinde aynı değerde tohumlayarak, rutinin her seferinde kesin bir sonuç verdiğini test edebilirsiniz.


PW sınıfı, esasen parolanın yaratılması gereken karakter havuzunu içeren bir sabit tutar. Onu alt sınıflandırarak ve tek bir karaktere sahip sabiti geçersiz kılarak test amacıyla belirsizliğin olduğu bir alanı ortadan kaldırmayı başardım. Çok teşekkürler.
GordonM

14

"Sözleşmeye" karşı test edin. Yöntemler "az ile 15 ila 20 karakter uzunluğunda şifreler oluşturur" olarak tanımlandığında, bu şekilde test edin

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

Ek olarak, nesli ayıklayabilirsiniz, böylece ona bağlı her şey, başka bir "statik" jeneratör sınıfı kullanılarak test edilebilir

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

Verdiğin regex faydalı oldu, bu yüzden testime ince ayarlı bir sürüm ekledim. Teşekkürler.
GordonM

6

Sizde var Password generatorve rastgele bir kaynağa ihtiyacınız var.

Soruda belirttiğiniz randomgibi, küresel devlet olduğu gibi deterministik olmayan bir çıktı ortaya çıkarır . Yani değer üretmek için sistemin dışındaki bir şeye erişir.

Tüm sınıflarınız için böyle bir şeyden asla kurtulamazsınız, fakat rastgele değerlerin yaratılması için şifre oluşturmayı ayırabilirsiniz.

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

}

Böyle bir kodu yapılandırırsanız, RandomSourcetestleriniz için alay edebilirsiniz .

% 100 test RandomSourceedemezsiniz ancak bu sorudaki değerleri test etmek için önerileriniz uygulanabilir ( rand->(1,26);Her zaman 1 ile 26 arasında bir sayı veren testler gibi) .


Bu harika bir cevap.
Nick Hodges

3

Parçacık fiziği Monte Carlo durumunda, önceden belirlenmiş rastgele bir tohumla deterministik olmayan rutini çağıran "birim testleri" {*} yazdım ve daha sonra istatistiksel bir kaç kez çalıştırıp kısıtlama ihlallerini kontrol ettim (enerji seviyeleri Girilen enerjinin üstüne erişilememeli, tüm geçitler önceden kaydedilmiş sonuçlara göre bir seviye vb. seçmeli ve gerilemelidir.


{*} Böyle bir test, birim testi için "testi hızlı yap" ilkesini ihlal eder; bu nedenle, bunları başka bir şekilde karakterize etmekte daha iyi hissedebilirsiniz: örneğin kabul testleri veya regresyon testleri. Yine de, birim sınama çerçevemi kullandım.


3

İki nedenden dolayı kabul edilen cevaba katılmıyorum :

  1. overfitting
  2. uygulanamazlık

( Birçok durumda bunun iyi bir cevap olabileceğine dikkat edin , ancak hepsinde değil, belki de çoğunda değil.)

Peki bununla ne demek istiyorum? Eh, fazladan takma ile tipik bir istatistiksel test problemi kastediyorum: fazladan kısıtlama, aşırı kısıtlanmış bir veri setine karşı stokastik bir algoritmayı test ettiğinizde olur. Sonra geri dönüp algoritma rafine varsa, örtülü olarak o (yanlışlıkla çok iyi eğitim verilerine uyacak yapacak sığdırmak test verilerine sizin algoritması), ancak diğer tüm veriler belki değil tüm (çünkü asla buna karşı testi) .

(Bu arada, bu her zaman birim testiyle gizlenen bir sorundur. Bu nedenle iyi testlerin tamamlanmış olması veya en azından belirli bir ünite için temsili olması ve genel olarak zor olması.)

Testlerinizi rastgele sayı üretecini takılabilir hale getirerek belirleyici yaparsanız, her zaman aynı çok küçük ve (genellikle) temsili olmayan veri kümelerine karşı test yaparsınız . Bu, verilerinizi çarpıtır ve işlevinizde yanlılığa yol açabilir.

İkinci nokta, pratiklik, stokastik değişken üzerinde herhangi bir kontrolünüz olmadığında ortaya çıkar. Bu genellikle rasgele sayı üreteçleriyle gerçekleşmez (“gerçek” rastgele bir kaynağa ihtiyacınız olmadığı sürece), ancak stokastik sizin sorununuza başka yollarla gizlice girdiğinde olabilir. Örneğin, eşzamanlı kodu test ederken: yarış durumlardır hep sen, stokastik olamaz (kolayca) onları deterministik olun.

Bu gibi durumlarda güven artırmanın tek yolu çok fazla test etmektir . Köpürtün, durulayın, tekrarlayın. Bu, belirli bir seviyeye kadar güven uyandırır (bu noktada ek test çalıştırmalarının dengelenmesi ihmal edilebilir hale gelir).


2

Aslında burada birden fazla sorumluluğun var. Birim testi ve özellikle TDD, bu tür bir şeyi vurgulamak için mükemmeldir.

Sorumluluklar:

1) Rastgele sayı üreteci. 2) Şifre formatlayıcı.

Parola biçimlendirici, rasgele sayı üretecini kullanır. Jeneratörü bir arayüz olarak yapıcı aracılığıyla formatlayıcınıza enjekte edin. Artık rasgele sayı üretecinizi (istatistiksel test) tamamen test edebilirsiniz ve biçimlendiriciyi sahte bir rasgele sayı üretecini enjekte ederek test edebilirsiniz.

Sadece daha iyi kod almakla kalmaz, daha iyi testler alırsınız.


2

Diğerlerinin daha önce de belirttiği gibi, rastgelelığı kaldırarak ünite testini gerçekleştirirsiniz .

Ayrıca, rasgele sayı üretecini yerinde bırakan, yalnızca sözleşmeyi test eden (şifre uzunluğu, izin verilen karakterler, ...) ve başarısızlık durumunda sistemi yeniden üretmenize izin verecek kadar bilgi atarsa ​​daha yüksek düzeyde bir test yapmak isteyebilirsiniz. rastgele testin başarısız olduğu bir örnekte belirtin.

Testin kendisinin tekrarlanamaz olması önemli değil - bu seferlik başarısızlığın nedenini bulabildiğiniz sürece.


2

Birçok ünite testi zorluğu, bağımlılığınızı ortadan kaldırmak için kodunuzu yeniden değiştirdiğinizde önemsiz hale gelir. Bir veritabanı, bir dosya sistemi, kullanıcı veya sizin durumunuzda rastgele bir kaynaktır.

Bakmanın bir başka yolu da, birim testlerinin "bu kod ne yapmak istediğimi yapıyor mu?" Sorusuna cevap vermesi gerektiğidir. Senin durumunda, kodun ne yapmak istediğini bilmiyorsun çünkü deterministik değil.

Bu akılla, mantığınızı küçük, kolay anlaşılır, kolay test edilmiş izolasyon parçalarına ayırın. Özellikle, girişi olarak bir rastgelelik kaynağı alan ve parolayı bir çıktı olarak üreten farklı bir yöntem (veya sınıf!) Yaratırsınız. Bu kod açıkça belirleyicidir.

Birim testinizde, her seferinde aynı rastgele olmayan girişi beslersiniz. Çok küçük rastgele akışlar için, testinizdeki değerleri sadece kodlayın. Aksi takdirde, testinizde RNG'ye sabit bir tohum verin.

Daha yüksek bir test seviyesinde ("kabul" veya "entegrasyon" veya başka bir şey olarak adlandırın) kodun gerçek bir rastgele kaynakla çalışmasına izin verin.


Bu cevap benim için çiviledi: Birinde gerçekten iki işlev vardı: rastgele sayı üreteci ve bu rastgele sayı ile bir şey yapan işlev. Basitçe yeniden kırıldım ve şimdi kodun karakteristik olmayan kısmını kolayca test edebilir ve rastgele bölüm tarafından oluşturulan parametreleri besleyebilirim. İşin güzel yanı, birim testimde (farklı setlerde) sabit parametreler besleyebilmemdir (standart kütüphaneden rastgele bir sayı üreteci kullanıyorum, bu yüzden yine de birim test etmiyorum).
nöronet

1

Yukarıdaki cevapların çoğu, rasgele sayı üretecini alay etmenin yol olduğunu gösteriyor, ancak ben sadece yerleşik mt_rand işlevini kullanıyordum. Alay etmeye izin vermek, sınıfın, inşaat sırasında enjekte edilmek üzere rasgele bir sayı üreteci gerektirecek şekilde yeniden yazılması anlamına gelirdi.

Ya da öyle düşünmüştüm!

Ad alanlarının eklenmesinin sonuçlarından biri, PHP işlevlerinde yerleşik alayın inanılmaz derecede zordan önemsiz derecede basit hale gelmesidir. SUT verilen bir ad alanındaysa, yapmanız gereken tek şey bu ad alanı altındaki birim testinde kendi mt_rand işlevinizi tanımlamaktır ve test süresince yerleşik PHP işlevi yerine kullanılacaktır.

İşte sonlandırılmış test paketi:

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

Bunu söyleyeceğimi düşündüm, çünkü PHP iç işlevlerini geçersiz kılmak, başıma gelmeyen ad alanlarının başka bir kullanımıdır. Bu konuda yardımcı olduğunuz için herkese teşekkürler.


0

Bu durumda eklemeniz gereken ek bir test var ve bu, şifre oluşturucuya yapılan tekrarlanan aramaların aslında farklı şifreler üretmesini sağlamak için bir tanesi. Konu güvenli bir şifre oluşturucuya ihtiyacınız varsa, aynı anda birden fazla konu kullanarak çağrıları da test etmelisiniz.

Bu, temel olarak, rastgele işlevinizi doğru bir şekilde kullanmanızı ve her aramada tekrar tohum almamanızı sağlar.


Aslında, sınıf ilk parolada getPassword () çağrısında parola oluşturulacak ve sonra mandallanacak şekilde tasarlanır, böylece nesnenin ömrü boyunca her zaman aynı parolayı döndürür. Test takımım zaten aynı şifre örneğindeki getPassword () çağrısının her zaman aynı şifre dizesini döndürdüğünü kontrol ediyor. İplik güvenliği gelince, bu PHP'de gerçekten bir endişe değil :)
GordonM
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.