我有一个关于使用PHPUnit来模拟类中的私有方法的问题。让我举一个例子:
class A {
public function b() {
// some code
$this->c();
// some more code
}
private function c(){
// some code
}
}
如何存储私有方法的结果以测试公共函数的更多代码部分。
解决部分阅读here
答案 0 :(得分:79)
通常你只是不测试或模仿私人&受保护的方法直接。
您要测试的是您班级的公开 API。其他所有内容都是您班级的实施细节,如果您更改了测试,则不应“破坏”测试。
当您发现“无法获得100%的代码覆盖率”时,这也会对您有所帮助,因为您的类中可能存在无法通过调用公共API执行的代码。
但如果你的班级看起来像这样:
class a {
public function b() {
return 5 + $this->c();
}
private function c() {
return mt_rand(1,3);
}
}
我可以看到需要模拟c()因为“随机”函数是全局状态而你无法测试它。
“干净?/详细?/过度复杂 - 可能?/我喜欢它通常”解决方案
class a {
public function __construct(RandomGenerator $foo) {
$this->foo = $foo;
}
public function b() {
return 5 + $this->c();
}
private function c() {
return $this->foo->rand(1,3);
}
}
现在不再需要模拟“c()”了,因为它不包含任何全局变量,你可以很好地测试。
如果您不想或不能从您的私人功能中删除全局状态(糟糕的事情,或者您对坏的定义可能有所不同),那么您可以对其进行测试模拟。
// maybe set the function protected for this to work
$testMe = $this->getMock("a", array("c"));
$testMe->expects($this->once())->method("c")->will($this->returnValue(123123));
并针对此模拟运行测试,因为您取出/ mock的唯一功能是“c()”。
引用“实用单位测试”一书:
“一般情况下,你不想为了测试而打破任何封装(或者像妈妈常说的那样,”不要暴露你的私人!“)。大多数时候,你应该能够通过行使其公共方法来测试一个类。如果隐藏在私人或受保护访问之后的重要功能,那可能是一个警告信号,那里有另一个班级在努力逃脱。“
答案 1 :(得分:25)
你可以test private methods 但你不能模拟(模拟)这种方法的运行。
此外,反射不允许您将私有方法转换为受保护或公共方法。 setAccessible仅允许您调用原始方法。
或者,您可以使用runkit重命名私有方法并包含“新实现”。但是,这些功能是实验性的,不建议使用它们。
答案 2 :(得分:24)
您可以在测试中使用reflection和setAccessible()
,以便您可以设置对象的内部状态,使其从私有方法返回您想要的内容。你需要使用PHP 5.3.2。
$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...
答案 3 :(得分:12)
您可以模拟受保护的方法,因此如果您可以将C转换为受保护,那么此代码将有所帮助。
$mock = $this->getMockBuilder('A')
->disableOriginalConstructor()
->setMethods(array('C'))
->getMock();
$response = $mock->B();
这肯定会奏效,对我有用。 然后,为了覆盖受保护的方法C,您可以使用反射类。
答案 4 :(得分:10)
假设您需要测试$ myClass-> privateMethodX($ arg1,$ arg2),您可以使用反射执行此操作:
$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
答案 5 :(得分:9)
以下是其他答案的变体,可用于将此类调用换成一行:
public function callPrivateMethod($object, $methodName)
{
$reflectionClass = new \ReflectionClass($object);
$reflectionMethod = $reflectionClass->getMethod($methodName);
$reflectionMethod->setAccessible(true);
$params = array_slice(func_get_args(), 2); //get all the parameters after $methodName
return $reflectionMethod->invokeArgs($object, $params);
}
答案 6 :(得分:5)
一种选择是c()
protected
而不是private
,然后继承并覆盖c()
。然后用你的子类测试。另一种选择是将c()
重构为一个可以注入A
的不同类(这称为依赖注入)。然后在单元测试中注入一个带有c()
模拟实现的测试实例。
答案 7 :(得分:5)
我为我的案例提出了这个通用课程:
/**
* @author Torge Kummerow
*/
class Liberator {
private $originalObject;
private $class;
public function __construct($originalObject) {
$this->originalObject = $originalObject;
$this->class = new ReflectionClass($originalObject);
}
public function __get($name) {
$property = $this->class->getProperty($name);
$property->setAccessible(true);
return $property->getValue($this->originalObject);
}
public function __set($name, $value) {
$property = $this->class->getProperty($name);
$property->setAccessible(true);
$property->setValue($this->originalObject, $value);
}
public function __call($name, $args) {
$method = $this->class->getMethod($name);
$method->setAccessible(true);
return $method->invokeArgs($this->originalObject, $args);
}
}
通过本课程,您现在可以轻松地和透明地解放任何对象上的所有私有函数/字段。
$myObject = new Liberator(new MyObject());
/* @var $myObject MyObject */ //Usefull for code completion in some IDEs
//Writing to a private field
$myObject->somePrivateField = "testData";
//Reading a private field
echo $myObject->somePrivateField;
//calling a private function
$result = $myObject->somePrivateFunction($arg1, $arg2);
如果性能很重要,可以通过缓存Liberator类中调用的属性/方法来改进它。
答案 8 :(得分:2)
另一种解决方案是将您的私有方法更改为受保护的方法,然后模拟。
$myMockObject = $this->getMockBuilder('MyMockClass')
->setMethods(array('__construct'))
->setConstructorArgs(array("someValue", 5))
->setMethods(array('myProtectedMethod'))
->getMock();
$response = $myMockObject->myPublicMethod();
其中myPublicMethod
调用myProtectedMethod
。遗憾的是我们无法使用私有方法执行此操作,因为setMethods
无法找到私有方法,因为它可以找到受保护的方法
答案 9 :(得分:0)
您可以使用PHP 7使用匿名类。
$mock = new class Concrete {
private function bob():void
{
}
};
在以前的PHP版本中,您可以创建一个扩展基类的测试类。
答案 10 :(得分:0)
当目标类既不是 static 也不是 final 时,我以接近 @jgmjgm 的方式解决这类问题,通过匿名和本地扩展原始类,因此不会在不同的测试用例中多次测试相同的方法:< /p>
$extent = new class ($args) extends A {
private function c() {
return $hardCodedValue;
}
};
然后我可以测试 $extent->b()
的行为,而无需再次执行可能很重的 c()
方法。