Php 如何通过更改引用传递的参数来单元测试调用具有副作用的函数的方法?
我有一个调用内置PHP函数的方法 我有一个PHPUnit测试用例来测试这个方法(它所做的只是验证随机生成的字符串是否有16字节长)Php 如何通过更改引用传递的参数来单元测试调用具有副作用的函数的方法?,php,unit-testing,random,phpunit,pass-by-reference,Php,Unit Testing,Random,Phpunit,Pass By Reference,我有一个调用内置PHP函数的方法 我有一个PHPUnit测试用例来测试这个方法(它所做的只是验证随机生成的字符串是否有16字节长) 我的问题是,如何测试$crypto_secure为FALSE且必须抛出异常的情况?由于此值是作为对openssl\u random\u pseudo\u bytes的引用传入和修改的,因此我不确定如何获得此执行路径的测试覆盖率。我的第一个想法是,可能有一个php.ini配置,我可以使用它强制openssl\u random\u pseudo\u bytes使用加密不
我的问题是,如何测试
$crypto_secure
为FALSE且必须抛出异常的情况?由于此值是作为对openssl\u random\u pseudo\u bytes
的引用传入和修改的,因此我不确定如何获得此执行路径的测试覆盖率。我的第一个想法是,可能有一个php.ini配置,我可以使用它强制openssl\u random\u pseudo\u bytes
使用加密不安全的算法(在测试用例中通过ini\u set
)。有什么建议吗?一个选项是将代码抽象出来,以便模拟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();
}
下面的答案是在假设接受两个不可变参数的情况下编写的。相反,如果随机字节是由强算法创建的,则第二个参数通过引用传递并提供反馈。鉴于这些信息,提供的答案是一种有效的方法,因为我们必须处理两个返回语句和一个副作用,这本质上使单元测试复杂化
在您的案例中,您不需要安全异常 您希望将
openssl\u random\u pseudo\u bytes
包装到您自己的自定义函数中,并希望将长度硬编码为16个字符,并始终使用true
调用openssl\u random\u pseudo\u bytes
。因此,您可以将类编写为:
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呢?然后它总是会失败。单元测试是一个黑盒。您不应该关心类在内部做什么。使用
crypto\u secure
是调用的一种配置,但是您的MyClass
正在抽象它的用法。您不需要测试配置/内部构件。您将crypto\u secure
设置为true。在你的情况下,你为什么要把它设为false?只是为了一个人为的测试用例?你能解释一下你想测试什么吗?因为这是一个xy问题。@k0pernikus我不知道你的意思是什么crypto_secure
不是调用的配置,它是openssl_pseudo_random_bytes用于通知调用方是否使用安全算法生成随机字节的标志。如果用于生成随机数的算法不安全,openssl_pseudo_random_字节将$crypto_secure
设置为FALSE
。我的错,我将其作为函数参数读取。现在我明白了为什么要将它作为变量传入,然后要检查它。我仍然认为,您不需要为检查创建单元测试。只要抛出异常就可以了。话虽如此,这也是为什么副作用对单元测试有害的一个典型例子。@k0pernikus这是一个很容易犯的错误,因为它在PHP中不是一个常见的模式(我的一位同事在代码审查时也有同样的困惑:),是的,我同意这是一个非常糟糕的模式。希望未来版本的PHP将有一个更为TDD友好的替代方案,而这一个似乎正是OP想要的,我认为:这并没有测试任何有意义的东西。单元测试不应模拟内部受保护的功能/分配。单元测试应该测试公共接口,内部的任何东西都应该被视为黑盒。是的,我同意,但如果您添加一个不模拟randomPseudoBytes
方法的测试,它可以作为参考,您是否已经测试了generateRandomBytes
方法的正常行为?您还可以争辩说,异常是公共API的一部分,因此值得测试我喜欢这种方法。使用返回回调修改引用是有意义的。不过,我同意@k0pernikus所说的,所以我将做一个修改:与其将其作为MyClass的受保护方法,不如将GeneratorAndomBytes放入其自己的外部助手类中,并将其实例作为依赖项注入MyClass中(我的一般规则是,每当你发现自己需要模拟一个受保护的方法时,就将它导出到它自己的helper类中)。谢谢大家的输入!但是openssl\u random\u pseudo\u bytes
设置了第二个参数,你知道吗?(我的意思是,它通过引用获取它并可能修改它。)@不要认为这是一个很好的观点。那么,检查这一点可能是有意义的,但我们也应该注意:对于[that boolean]来说,这是很少见的这可能是错误的,但有些系统可能已损坏或陈旧。
是的,当然。问题似乎更多的是如何在测试中模拟旧的、已损坏的系统,以验证在这种情况下是否引发异常。不过,我当然不知道如何做。
class MyClass
{
public function generateRandomBytes()
{
return openssl_random_pseudo_bytes(16, true);
}
}
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);
}
}