Одиничні методи випробування з невизначеним виходом


37

У мене є клас, призначений для генерування випадкового пароля довжини, яка також є випадковою, але обмежена між визначеним мінімумом та максимальною довжиною.

Я будував одиничні тести і наткнувся на цікавий маленький корч з цим класом. Вся ідея одиничного тесту полягає в тому, що він повинен бути повторюваним. Якщо ви запускаєте тест сто разів, він повинен давати однакові результати сто разів. Якщо ви залежите від певного ресурсу, який може бути, а може і не бути, або не бути в початковому стані, який ви очікуєте, тоді ви маєте на меті знущатися над відповідним ресурсом, щоб переконатися, що ваш тест справді завжди повторюється.

Але що робити у випадках, коли SUT повинен генерувати невизначений вихід?

Якщо я зафіксую мінімальну та максимальну довжину до одного і того ж значення, я можу легко перевірити, чи створений пароль має очікувану довжину. Але якщо я вкажу діапазон прийнятної довжини (скажімо, 15 - 20 символів), то у вас зараз виникає проблема, що ви могли пройти тест сто разів і отримати 100 пропусків, але на 101-му запуску ви можете повернути рядок з 9 символів.

Що стосується класу паролів, який є досить простим по своїй суті, він не повинен виявляти величезної проблеми. Але це змусило мене задуматися про загальний випадок. Яка стратегія, яку зазвичай приймають як найкращу, має бути застосована під час роботи з SUT, які проектують невизначений вихід?


9
Чому закриті голоси? Я думаю, що це цілком справедливе питання.
Марк Бейкер

Ага, дякую за коментар. Я навіть цього не помічав, але зараз мені цікаво те саме. Єдине, про що я міг придумати, це стосується загального випадку, а не конкретного, але я можу просто опублікувати джерело для вищевказаного класу паролів і запитати "Як я перевіряю цей клас?" замість "Як перевірити будь-який невизначений клас?"
GordonM

1
@MarkBaker Тому що більшість запитань про тестування є на programmers.se. Це голосування за міграцію, а не для закриття питання.
Ikke

Відповіді:


20

"Недетермінований" вихід повинен мати спосіб стати детермінованим для цілей одиничного тестування. Один із способів вирішити випадковість - дозволити заміну випадкового двигуна. Ось приклад (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);

Ви можете зробити спеціалізовану тестову версію функції, яка повертає будь-яку послідовність чисел, які ви хочете переконатися, що тест є повністю повторюваним. У реальній програмі у вас може бути реалізація за замовчуванням, яка може бути резервною, якщо не переопрацьована.


1
Усі відповіді дали хороші пропозиції, які я використав, але я вважаю, що це те, що я вважаю основним питанням, щоб отримати його.
GordonM

1
Досить сильно прибиває його до голови. Незважаючи на недетермінованість, все ж існують межі.
surfasb

21

Фактичний вихідний пароль не може бути визначений щоразу, коли метод виконується, але він все одно матиме визначені функції, які можна перевірити, такі як мінімальна довжина, символи, що потрапляють у визначений набір символів тощо.

Ви також можете перевірити, що підпрограма щоразу повертає визначений результат, кожного разу висіваючи генератор паролів з однаковим значенням.


Клас PW підтримує константу, що по суті є пулом символів, з якого повинен бути створений пароль. Підкласифікувавши його та замінивши константу на один символ, мені вдалося усунути одну область невизначеності для тестування. Тож спасибі
GordonM

14

Тест на "контракт". Коли методи визначені як "генерує паролі довжиною від 15 до 20 символів за допомогою az", протестуйте його таким чином

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

Регекс, який ви дали, виявився корисним, тому я включив у свій тест виправлену версію. Спасибі.
GordonM

6

У вас є Password generatorі вам потрібно випадкове джерело.

Як ви сказали у запитанні, а randomдає недетермінований результат, оскільки це глобальний стан . Це означає, що він отримує доступ до чогось поза системою для отримання значень.

Ви ніколи не можете позбутися чогось подібного для всіх своїх класів, але ви можете розділити генерацію паролів для створення випадкових значень.

<?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ваших тестів.

Ви не зможете на 100% перевірити, RandomSourceале запропоновані вами пропозиції щодо тестування значень у цьому запиті можна застосувати до нього (як і тестування, яке rand->(1,26);завжди повертає число від 1 до 26.


Це чудова відповідь.
Нік Ходжес

3

Що стосується фізики частинок Монте-Карло, я написав "одиничні тести" {*}, які викликають недетерміновану процедуру із заданим випадковим насінням , а потім запускають статистичну кількість разів і перевіряють на порушення обмежень (енергетичні рівні вище вхідної енергії має бути недоступним, усі проходи повинні вибрати деякий рівень тощо) та регресії проти раніше записаних результатів ..


{*} Такий тест порушує принцип "зробіть тест швидким" для одиничного тестування, тож вам може бути краще, характеризуючи їх іншим способом: наприклад, приймальні тести або регресійні тести. І все-таки я використав свою рамку тестування одиниць.


3

Я маю згоду з прийнятою відповіддю з двох причин:

  1. Переобладнання
  2. Непроявність

(Зауважте, що це може бути хорошою відповіддю за багатьох обставин, але не у всіх, а може, і не в більшості.)

То що я маю на увазі під цим? Ну, маючи на увазі, я маю на увазі типову проблему статистичного тестування: перевиконання відбувається, коли ви протестуєте стохастичний алгоритм на надмірно обмеженому наборі даних. Якщо ви повернетесь назад і вдосконалите свій алгоритм, ви будете явно змусити його добре підходити до навчальних даних (ви випадково вписуєте свій алгоритм до тестових даних), але всі інші дані, можливо, зовсім не такі (тому що ви ніколи не тестуєте проти нього) .

(Між іншим, це завжди проблема, яка криється з одиничним тестуванням. Ось чому хороші тести є повними , або принаймні репрезентативними для даної одиниці, і це важко загалом.)

Якщо ви робите тести детермінованими, роблячи підключення генератора випадкових чисел, ви завжди перевіряєте той самий дуже маленький і (зазвичай) нерепрезентативний набір даних. Це скаже ваші дані і може призвести до упередженості вашої функції.

Другий момент, нездійсненність, виникає, коли ви не маєте ніякого контролю над стохастичною змінною. Зазвичай це не відбувається з генераторами випадкових чисел (якщо вам не потрібен "реальний" джерело випадкових випадків), але це може статися, коли стохастики проникнуть у вашу проблему іншими способами. Наприклад, під час тестування паралельного коду: умови гонки завжди стохастичні, ви не можете (легко) зробити їх детермінованими.

Єдиний спосіб підвищити впевненість у цих випадках - це тестувати багато . Натріть, промийте, повторіть. Це піднімає впевненість до певного рівня (в цей момент компроміс на додаткові тестові пробіжки стає незначним).


2

Ви фактично несете тут декілька обов'язків. Тестування приладів, зокрема TDD, відмінно підходить для висвітлення подібних речей.

Обов'язки:

1) Генератор випадкових чисел 2) Форматор паролів.

Форматор паролів використовує генератор випадкових чисел. Введіть генератор у ваш форматер через його конструктор як інтерфейс. Тепер ви можете повністю протестувати свій генератор випадкових чисел (статистичний тест), і ви можете протестувати форматер, ввівши знущений генератор випадкових чисел.

Ви не тільки отримуєте кращий код, ви отримуєте кращі тести.


2

Як уже згадували інші, ви перевіряєте цей код, видаляючи випадковість.

Ви також можете провести тест вищого рівня, який залишає генератор випадкових чисел на місці, тестує лише контракт (довжина пароля, дозволені символи, ...) і, у разі відмови, скидає достатньо інформації, що дозволяє відтворити систему в одному випадку, коли випадковий тест не вдався.

Не має значення, що тест сам по собі не повторюється - доки ви зможете знайти причину, чому він цього разу не вдався.


2

Багато труднощів з тестуванням одиниць стають дрібницями, коли ви перестроюєте код, щоб розділити залежність. База даних, файлова система, користувач або у вашому випадку джерело випадковості.

Інший спосіб дивитися на те, що одиничні тести повинні відповісти на питання "чи робить цей код те, що я маю намір робити?". У вашому випадку ви не знаєте, що ви маєте намір зробити, оскільки код не детермінований.

Зважаючи на це, розділіть свою логіку на невеликі, легко зрозумілі, легко перевірені в ізоляції частини. Зокрема, ви створюєте чіткий метод (або клас!), Який приймає джерело випадковості як своє введення та виробляє пароль як вихід. Цей код явно детермінований.

Під час тестування одиниці ви кожного разу подаєте на нього той самий не зовсім випадковий ввід. Для дуже малих випадкових потоків просто жорстко кодуйте значення у вашому тесті. В іншому випадку забезпечте постійне насіння в РГГ у своєму тесті.

На вищому рівні тестування (називайте це "прийняття" або "інтеграція" чи будь-яке інше) ви дозволяєте коду працювати з справжнім випадковим джерелом.


Ця відповідь прибила мене: я справді мав дві функції в одній: генератор випадкових чисел і функцію, яка щось робила з цим випадковим числом. Я просто відреставрувався і тепер легко можу перевірити недетерміновану частину коду та подавати його параметри, породжені випадковою частиною. Хороша річ у тому, що я можу потім подати його (різні набори) фіксованих параметрів у своєму тесті одиниці (я використовую генератор випадкових чисел зі стандартної бібліотеки, так що не тестування одиниць у будь-якому випадку).
нейронет

1

Більшість наведених вище відповідей вказують на те, що глузувати генератор випадкових чисел - це шлях, але я просто використовував вбудовану функцію mt_rand. Дозвіл глузування означало б переписання класу, щоб вимагати введення генератора випадкових чисел під час побудови.

Або так я подумав!

Одним із наслідків додавання просторів імен є те, що глузування з вбудованих функцій 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 - це ще одне використання просторів імен, які мені просто не спадали на думку. Дякую всім за допомогу в цьому.


0

Існує додатковий тест, який слід включити до цієї ситуації, і це один, щоб переконатися, що багаторазові дзвінки до генератора паролів насправді створюють різні паролі. Якщо вам потрібен безпечний потоковий генератор паролів, ви також повинні протестувати одночасні дзвінки за допомогою декількох потоків.

Це в основному забезпечує правильне використання випадкової функції, а не повторне висівання кожного дзвінка.


Насправді клас створений таким чином, що пароль формується під час першого виклику getPassword (), а потім фіксується, тому він завжди повертає той самий пароль протягом життя об’єкта. Мій тестовий набір вже перевіряє, що багаторазові дзвінки до getPassword () в одному екземплярі пароля завжди повертають ту саму рядок пароля. Що стосується безпеки потоків, то це не дуже хвилює PHP :)
GordonM
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.