Методы модульного тестирования с неопределенным выходом

37

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

Я создаю модульные тесты и столкнулся с интересной небольшой проблемой с этим классом. Вся идея модульного теста заключается в том, что он должен быть повторяемым. Если вы запускаете тест сто раз, он должен давать те же результаты сто раз. Если вы зависите от какого-либо ресурса, который может присутствовать, а может и не быть, или может быть, а может и не находиться в исходном состоянии, которое вы ожидаете, тогда вы должны высмеять этот ресурс, чтобы убедиться, что ваш тест действительно всегда повторяется.

Но что делать в случаях, когда SUT должен генерировать неопределенный результат?

Если я установлю минимальную и максимальную длину на одно и то же значение, тогда я могу легко проверить, что сгенерированный пароль имеет ожидаемую длину. Но если я укажу диапазон допустимых длин (скажем, 15–20 символов), то у вас теперь есть проблема, что вы можете выполнить тест сто раз и получить 100 проходов, но при 101-м запуске вы можете получить обратно 9-символьную строку.

В случае с классом пароля, который по своей сути довольно прост, он не должен вызывать огромных проблем. Но это заставило меня задуматься об общем случае. Какую стратегию обычно принимают в качестве наилучшей стратегии при работе с ТРИ, которые генерируют неопределенный выход по проекту?

GordonM
источник
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);

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

bobbymcr
источник
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; }
}
KingCrunch
источник
Выражение, которое вы дали, оказалось полезным, поэтому я включил в свой тест измененную версию. Спасибо.
GordonM
6

У вас есть Password generatorи вам нужен случайный источник.

Как вы указали в вопросе, a 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).

edorian
источник
Это отличный ответ.
Ник Ходжес
3

В случае физики элементарных частиц Монте-Карло я написал «модульные тесты» {*}, которые вызывают недетерминированную процедуру с заранее установленным случайным начальным числом, а затем запускают статистическое число раз и проверяют нарушения ограничений (энергетические уровни выше входная энергия должна быть недоступна, все проходы должны выбирать некоторый уровень и т. д.) и регрессии против ранее записанных результатов.


{*} Такой тест нарушает принцип «сделай тест быстрым» для модульного тестирования, так что ты можешь чувствовать себя лучше, охарактеризовав его другим способом: например, приемочные тесты или регрессионные тесты. Тем не менее, я использовал мою систему модульного тестирования.

dmckee
источник
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 - это еще одно использование для пространств имен, которое мне просто не пришло в голову. Спасибо всем за помощь в этом.

GordonM
источник
0

В этой ситуации следует включить дополнительный тест, который должен гарантировать, что повторные вызовы генератора паролей действительно выдают разные пароли. Если вам нужен потокобезопасный генератор паролей, вы также должны тестировать одновременные вызовы, используя несколько потоков.

Это в основном гарантирует, что вы используете вашу случайную функцию правильно, а не повторное заполнение при каждом вызове.

Torbjørn
источник
На самом деле, класс спроектирован так, что пароль генерируется при первом вызове getPassword (), а затем фиксируется, поэтому он всегда возвращает один и тот же пароль в течение всего времени жизни объекта. Мой набор тестов уже проверяет, что несколько вызовов getPassword () для одного и того же экземпляра пароля всегда возвращают одну и ту же строку пароля. Что касается безопасности потоков, то это не очень
важно