Как выполнить модульное тестирование метода, вызывающего функцию с побочным эффектом, путем изменения аргумента, переданного по ссылке? - PullRequest
0 голосов
/ 27 сентября 2018

У меня есть метод, который делает вызов встроенной функции PHP, openssl_random_pseudo_bytes.

public function generateRandomBytes()
{
    $crypto_secure = TRUE;

    // $crypto_secure is passed by reference and will be set to FALSE by
    // openssl_random_pseudo_bytes if it uses an insecure algorithm
    $random_bytes = openssl_random_pseudo_bytes(16, $crypto_secure);
    if (!$crypto_secure)
    {
        throw new Security_Exception('Random bytes not generated by a cryptographically secure PRNG algorithm');
    }
    return $random_bytes;
}

У меня есть один тестовый пример PHPUnit для тестирования этого метода (все, что он делаетэто проверить, что длина случайно сгенерированной строки составляет 16 байтов).

public function testRandomBytesLength()
{
    $myclass = new MyClass();

    $this->assertEquals(16, strlen($myclass->generateRandomBytes()));
}

Мой вопрос: как мне проверить случай, когда $crypto_secure равно FALSE и должно быть выдано исключение?Поскольку это значение передается и изменяется как ссылка на openssl_random_pseudo_bytes, я не уверен, как мне получить тестовое покрытие для этого пути выполнения.Сначала я подумал, что, возможно, есть конфигурация php.ini, которую я мог бы использовать, чтобы заставить openssl_random_pseudo_bytes использовать криптографически небезопасный алгоритм (через ini_set в тестовом примере).Есть предложения?

Ответы [ 2 ]

0 голосов
/ 27 сентября 2018

Этот ответ ниже был написан в предположении, что openssl-random-pseudo-bytes принимает два неизменных аргумента.Вместо этого второй аргумент передается по ссылке и дает обратную связь, если случайные байты были созданы сильным алгоритмом.Учитывая эту информацию, ответ, предоставленный Робби Аверилл , является верным подходом, поскольку приходится иметь дело в основном с двумя операторами возврата и побочным эффектом, который по своей сути усложняет юнит-тестирование.


Вам не нужно исключение безопасности в вашем случае.

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

class MyClass
{
    public function generateRandomBytes()
    {
        return openssl_random_pseudo_bytes(16, true);
    }
}

Единственный значимый тест здесь - это проверить, что длина возвращаемой строки составляет 16 символов.И вы рассмотрели этот случай.


Чтобы показать ненужность создания исключения, вы бы предпочли добавить флаг в конструктор или в качестве параметра:

class MyClass
{
    /**
     * @var bool
     */
    private $beSecure;

    public function __construct(bool $beSecure)
    {
        $this->beSecure = $beSecure;
    }


    /**
     * @return string
     * @throws Exception
     */
    public function generateRandomBytes(): string
    {
        if (!$this->beSecure) {
            // will always throw if false is injected, why would we do that?
            throw new Exception("I AM NOT SECURE!");
        }

        return openssl_random_pseudo_bytes(16, true);
    }
}

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

0 голосов
/ 27 сентября 2018

Один из вариантов - абстрагировать ваш код, чтобы вы могли смоделировать возвращаемое значение метода openssl:

public function generateRandomBytes()
{
    $crypto_secure = TRUE;
    $random_bytes = $this->randomPseudoBytes(16, $crypto_secure);
    if (!$crypto_secure)
    {
        throw new Security_Exception('Random bytes not generated by a cryptographically secure PRNG algorithm');
    }
    return $random_bytes;
}

protected function randomPseudoBytes($length, &$crypto_secure)
{
    return openssl_random_pseudo_bytes(16, $crypto_secure);
}

Затем вы можете управлять оберткой вокруг основной функции, чтобы проверить, как ваш код реагирует на нее.меняется:

/**
 * @expectedException Security_Exception
 * @expectedExceptionMessage Random bytes not generated by a cryptographically secure PRNG algorithm
 */
public function testCryptoIsNotSecure()
{
    $myclass = $this->getMockBuilder(MyClass::class)->setMethods(['randomPseudoBytes'])->getMock();

    $myclass->expects($this->once())
        ->method('randomPseudoBytes')
        ->will($this->returnCallback(function ($length, &$secure) {
            // Mock variable assignment via reference
            $secure = false;
        });

    $myclass->generateRandomBytes();
}
...