如何通过更改按引用传递的参数来对调用具有副作用的函数的方法进行单元测试?

Ata*_*xia 4 php random phpunit unit-testing pass-by-reference

我有一个方法可以调用内置的 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;
}
Run Code Online (Sandbox Code Playgroud)

我有一个 PHPUnit 测试用例来测试这个方法(它所做的只是验证随机生成的字符串是 16 个字节长)。

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

    $this->assertEquals(16, strlen($myclass->generateRandomBytes()));
}
Run Code Online (Sandbox Code Playgroud)

我的问题是,如何测试$crypto_secureFALSE 并且必须抛出异常的情况?由于此值作为对 的引用传入并修改openssl_random_pseudo_bytes,因此我不确定如何获得此执行路径的测试覆盖率。我的第一个想法是,也许有一个 php.ini 配置可以用来强制openssl_random_pseudo_bytes使用加密不安全的算法(通过ini_set在测试用例中)。有什么建议?

Rob*_*ill 5

一种选择是将您的代码抽象出来,以便您可以模拟 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);
}
Run Code Online (Sandbox Code Playgroud)

然后你可以控制核心函数周围的包装器来测试你的代码如何对它的变化做出反应:

/**
 * @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();
}
Run Code Online (Sandbox Code Playgroud)

  • 虽然这似乎正是 OP 想要的,但我认为:这并没有测试任何有意义的东西。单元测试不应模拟内部受保护的功能/分配。单元测试应该测试公共接口,内部的任何东西都应该被视为黑匣子。 (3认同)
  • 您也可以争辩说异常是公共 API 的一部分,因此值得测试 (3认同)
  • 我喜欢这种方法。使用返回回调来修改引用是有意义的。我同意@k0pernikus 所说的,所以我会做一个修改:与其将它变成 MyClass 的受保护方法,我认为将 generateRandomBytes 放入它自己的外部助手类并注入它的一个实例是有意义的对 MyClass 的依赖(我的一般规则是,每当您发现自己需要模拟受保护的方法时,将其导出到其自己的帮助程序类)。感谢大家的意见和建议! (2认同)