Metody testowania jednostkowego z nieokreślonym wyjściem


37

Mam klasę, która ma generować losowe hasło o długości, która jest również losowa, ale ograniczona do określonych między minimalną a maksymalną długością.

Tworzę testy jednostkowe i natknąłem się na interesującą małą przeszkodę w tej klasie. Cała idea testu jednostkowego polega na tym, że powinien on być powtarzalny. Jeśli uruchomisz test sto razy, powinien on dać te same wyniki sto razy. Jeśli zależysz od jakiegoś zasobu, który może, ale nie musi, znajdować się w początkowym stanie, którego oczekujesz, masz za zadanie kpić z danego zasobu, aby upewnić się, że Twój test jest zawsze powtarzalny.

Ale co z przypadkami, w których SUT ma generować nieokreślony wynik?

Jeśli poprawię minimalną i maksymalną długość na tę samą wartość, mogę łatwo sprawdzić, czy wygenerowane hasło ma oczekiwaną długość. Ale jeśli podam zakres akceptowalnych długości (powiedzmy 15-20 znaków), wtedy masz problem, że możesz uruchomić test sto razy i uzyskać 100 przebiegów, ale przy 101 biegu możesz odzyskać ciąg 9 znaków.

W przypadku klasy haseł, która jest dość prosta w swoim rdzeniu, nie powinna stanowić dużego problemu. Ale pomyślałem o ogólnym przypadku. Jaka strategia jest zwykle akceptowana jako najlepsza do wyboru, gdy mamy do czynienia z SUT, które generują nieokreślony wynik z projektu?


9
Dlaczego bliskie głosy? Myślę, że jest to całkowicie uzasadnione pytanie.
Mark Baker

Dzięki za komentarz. Nawet tego nie zauważyłem, ale teraz zastanawiam się nad tym samym. Jedyne, co mogłem wymyślić, to raczej ogólny przypadek niż konkretny, ale mogłem po prostu opublikować źródło wyżej wspomnianej klasy hasła i zapytać „Jak przetestować tę klasę?”. zamiast „Jak przetestować jakąś nieokreśloną klasę?”
GordonM,

1
@MarkBaker Ponieważ większość najtrafniejszych pytań znajduje się na stronie programmers.se. To głosowanie za migracją, a nie zamknięcie pytania.
Ikke

Odpowiedzi:


20

Wyjście „niedeterministyczne” powinno mieć sposób, by stać się deterministyczne na potrzeby testów jednostkowych. Jednym ze sposobów radzenia sobie z losowością jest umożliwienie wymiany losowego silnika. Oto przykład (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);

Możesz stworzyć specjalną testową wersję funkcji, która zwraca dowolną sekwencję liczb, które chcesz upewnić się, że test jest w pełni powtarzalny. W prawdziwym programie możesz mieć domyślną implementację, która może być rezerwowa, jeśli nie zostanie zastąpiona.


1
Wszystkie udzielone odpowiedzi zawierały dobre sugestie, które wykorzystałem, ale myślę, że jest to jedna z najważniejszych kwestii, więc otrzymuję akceptację.
GordonM,

1
Prawie wbija go w głowę. Choć niedeterministyczne, nadal istnieją granice.
surfasb

21

Rzeczywiste hasło wyjściowe może nie zostać określone za każdym razem, gdy metoda jest wykonywana, ale nadal będzie miało określone funkcje, które można przetestować, takie jak minimalna długość, znaki mieszczące się w określonym zestawie znaków itp.

Możesz także przetestować, czy procedura zwraca za każdym razem określony wynik, za każdym razem zapełniając generator haseł tą samą wartością.


Klasa PW utrzymuje stałą, która jest zasadniczo pulą znaków, z których powinno zostać wygenerowane hasło. Podklasując go i zastępując stałą jednym znakiem, udało mi się wyeliminować jeden obszar niedeterminacji na potrzeby testowania. Więc dziękuję.
GordonM,

14

Test pod kątem „umowy”. Gdy metody są zdefiniowane jako „generuje hasła o długości od 15 do 20 znaków z az”, przetestuj to w ten sposób

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

Dodatkowo możesz wyodrębnić generację, aby wszystko, co na niej polegało, mogło zostać przetestowane przy użyciu innej „statycznej” klasy generatora

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

Wyrażenie regularne, które podałeś okazało się przydatne, więc dołączyłem poprawioną wersję do mojego testu. Dzięki.
GordonM,

6

Masz Password generatori potrzebujesz losowego źródła.

Jak powiedziałeś w pytaniu, a randomtworzy niedeterministyczny wynik, ponieważ jest to stan globalny . Oznacza to, że uzyskuje dostęp do czegoś poza systemem, aby wygenerować wartości.

Nigdy nie możesz pozbyć się czegoś takiego dla wszystkich swoich klas, ale możesz oddzielić generowanie haseł do tworzenia losowych wartości.

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

}

Jeśli skonstruujesz taki kod, możesz wyśmiewać jego RandomSourcetesty.

Nie będziesz w stanie w 100% przetestować, RandomSourceale możesz zastosować do niego sugestie dotyczące testowania wartości z tego pytania (na przykład testowanie, które rand->(1,26);zawsze zwraca liczbę od 1 do 26).


To świetna odpowiedź.
Nick Hodges

3

W przypadku fizyki cząstek elementarnych Monte Carlo napisałem „testy jednostkowe” {*}, które odwołują się do niedeterministycznej rutyny ze wstępnie ustawionym losowym ziarnem , a następnie przeprowadzam statystyczną liczbę razy i sprawdzam, czy nie występują ograniczenia (poziomy energii) powyżej energii wejściowej musi być niedostępny, wszystkie przejścia muszą wybrać jakiś poziom itp.) oraz regresje względem wcześniej zarejestrowanych wyników.


{*} Taki test narusza zasadę „wykonaj szybki test” w przypadku testów jednostkowych, dlatego możesz lepiej je scharakteryzować w inny sposób: na przykład testy akceptacyjne lub testy regresyjne. Mimo to korzystałem z mojej platformy do testów jednostkowych.


3

Muszę się nie zgodzić z przyjętą odpowiedzią z dwóch powodów:

  1. Nadmierne dopasowanie
  2. Niewykonalność

(Zauważ, że może to być dobra odpowiedź w wielu okolicznościach, ale nie we wszystkich, a może nie w większości).

Co mam przez to na myśli? Przez nadmierne dopasowanie rozumiem typowy problem testowania statystycznego: nadmierne dopasowanie ma miejsce, gdy testujesz algorytm stochastyczny w stosunku do nadmiernie ograniczonego zestawu danych. Jeśli następnie wrócisz i dopracujesz algorytm, domyślnie sprawisz, że będzie on bardzo dobrze pasował do danych treningowych (przypadkowo dopasujesz swój algorytm do danych testowych), ale wszystkie inne dane mogą wcale nie być (ponieważ nigdy nie testujesz go) .

(Nawiasem mówiąc, zawsze jest to problem, który czai się przy testowaniu jednostkowym. Dlatego dobre testy są kompletne lub przynajmniej reprezentatywne dla danej jednostki, i ogólnie jest to trudne.)

Jeśli uczynisz swoje testy deterministycznymi, umożliwiając podłączenie generatora liczb losowych, zawsze testujesz na tym samym bardzo małym i (zwykle) niereprezentatywnym zestawie danych. To wypacza twoje dane i może prowadzić do stronniczości w twojej funkcji.

Drugi punkt, niewykonalność, powstaje, gdy nie masz żadnej kontroli nad zmienną stochastyczną. Zwykle nie dzieje się tak w przypadku generatorów liczb losowych (chyba że potrzebujesz „prawdziwego” źródła losowego), ale może się zdarzyć, gdy stochastycy wkradną się do twojego problemu innymi sposobami. Na przykład podczas testowania współbieżnego kodu: warunki wyścigu są zawsze stochastyczne, nie można (łatwo) uczynić ich deterministycznymi.

Jedynym sposobem na zwiększenie zaufania w takich przypadkach jest przeprowadzenie wielu testów . Spłucz, spłucz, powtórz. Zwiększa to pewność do pewnego poziomu (w którym momencie kompromis dla dodatkowych testów staje się znikomy).


2

Masz tutaj wiele obowiązków. Testy jednostkowe, a zwłaszcza TDD, świetnie nadają się do podkreślenia tego rodzaju rzeczy.

Obowiązki to:

1) Generator liczb losowych. 2) Formatator hasła.

Formatyzator haseł używa generatora liczb losowych. Wstaw generator do formatera za pomocą jego konstruktora jako interfejsu. Teraz możesz w pełni przetestować generator liczb losowych (test statystyczny) i przetestować formatyzator, wstrzykując wyśmiewany generator liczb losowych.

Nie tylko dostajesz lepszy kod, ale także lepsze testy.


2

Jak już wspomniano inni, testujesz ten kod, usuwając losowość.

Możesz również chcieć mieć test wyższego poziomu, który pozostawia generator liczb losowych na miejscu, testuje tylko kontrakt (długość hasła, dozwolone znaki, ...), a w przypadku awarii zrzuca wystarczającą ilość informacji, aby umożliwić odtworzenie systemu stan w jednym przypadku, w którym losowy test nie powiódł się.

Nie ma znaczenia, że ​​sam test nie jest powtarzalny - o ile tylko znajdziesz przyczynę, dla której ten raz się nie powiódł.


2

Wiele trudności w testowaniu jednostkowym staje się trywialnych po przefakturowaniu kodu w celu zerwania zależności. Baza danych, system plików, użytkownik lub, w twoim przypadku, źródło losowości.

Innym sposobem patrzenia jest to, że testy jednostkowe mają odpowiedzieć na pytanie „czy ten kod robi to, co zamierzam?”. W twoim przypadku nie wiesz, co zamierzasz zrobić, ponieważ kod jest niedeterministyczny.

Mając to na uwadze, podziel swoją logikę na małe, łatwe do zrozumienia, łatwe do przetestowania w izolacji części. W szczególności tworzysz odrębną metodę (lub klasę!), Która pobiera źródło losowości jako dane wejściowe i tworzy hasło jako dane wyjściowe. Ten kod jest wyraźnie deterministyczny.

W teście jednostkowym za każdym razem podajesz to samo niezbyt losowe dane wejściowe. W przypadku bardzo małych losowych strumieni po prostu zakoduj wartości w swoim teście. W przeciwnym razie zapewnij stały test RNG w teście.

Na wyższym poziomie testowania (nazwij to „akceptacją” lub „integracją” lub czymkolwiek innym), pozwolisz, aby kod działał z prawdziwym losowym źródłem.


Ta odpowiedź mnie zaskoczyła: naprawdę miałem dwie funkcje w jednym: generator liczb losowych i funkcję, która zrobiła coś z tą liczbą losową. Po prostu dokonałem refaktoryzacji i teraz mogę łatwo przetestować niedeterministyczną część kodu i podać parametry generowane przez część losową. Fajną rzeczą jest to, że mogę wtedy karmić to (różne zestawy) ustalonymi parametrami w moim teście jednostkowym (używam generatora liczb losowych ze standardowej biblioteki, więc i tak nie testuję jednostkowo).
neuronet

1

Większość powyższych odpowiedzi wskazuje, że najlepiej jest wyśmiewać generator liczb losowych, ale po prostu użyłem wbudowanej funkcji mt_rand. Zezwolenie na kpowanie oznaczałoby przepisanie klasy, aby wymagała wstrzyknięcia generatora liczb losowych w czasie budowy.

A przynajmniej tak myślałem!

Jedną z konsekwencji dodania przestrzeni nazw jest to, że drwiny wbudowane w funkcje PHP przeszły z niezwykle trudnej do banalnie prostej. Jeśli SUT znajduje się w danej przestrzeni nazw, wystarczy, że zdefiniujesz własną funkcję mt_rand w teście jednostkowym w tej przestrzeni nazw i będzie ona używana zamiast wbudowanej funkcji PHP na czas trwania testu.

Oto sfinalizowany pakiet testowy:

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

Pomyślałem, że o tym wspomnę, ponieważ zastąpienie wewnętrznych funkcji PHP to kolejne zastosowanie dla przestrzeni nazw, które po prostu mi się nie przydarzyły. Dziękujemy wszystkim za pomoc w tym.


0

W tej sytuacji należy wykonać dodatkowy test, który ma na celu upewnienie się, że wielokrotne połączenia z generatorem haseł faktycznie generują różne hasła. Jeśli potrzebujesz generatora haseł bezpiecznych dla wątków, powinieneś również przetestować jednoczesne połączenia przy użyciu wielu wątków.

Zasadniczo zapewnia to prawidłowe korzystanie z losowej funkcji, a nie ponowne inicjowanie przy każdym wywołaniu.


W rzeczywistości klasa została zaprojektowana w taki sposób, że hasło jest generowane przy pierwszym wywołaniu getPassword (), a następnie zatrzaśnięte, więc zawsze zwraca to samo hasło przez cały okres istnienia obiektu. Mój zestaw testów już sprawdza, czy wiele wywołań funkcji getPassword () w tej samej instancji hasła zawsze zwraca ten sam ciąg hasła. Jeśli chodzi o bezpieczeństwo wątków, to nie jest tak naprawdę problem w PHP :)
GordonM
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.