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

时间:2018-09-27 15:59:49

标签: php unit-testing random phpunit 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;
}

我有一个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 :(得分:4)

一种选择是抽象出代码,以便可以模拟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();
}

答案 1 :(得分:1)

下面的答案是在openssl-random-pseudo-bytes接受两个不可混淆的参数的前提下编写的。相反,第二个参数由引用传递,如果随机字节是由强大算法创建的,则给出反馈。有了这些信息,Robbie Averill提供的答案是一种有效的方法,因为必须处理两个基本的返回声明和一个副作用,这从本质上使单元测试变得复杂。


在您的情况下,您不需要安全例外。

您想将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?然后它将总是失败。