我有一个关于使用PHPUnit模拟类中的私有方法的问题。让我举一个例子:
class A {
public function b() {
// some code
$this->c();
// some more code
}
private function c(){
// some code
}
}
我该如何对私有方法的结果进行存根以测试公共函数的更多代码部分。
在这里解决了部分阅读
Answers:
通常,您只是不直接测试或嘲笑私有和受保护的方法。
您要测试的是您的类的公共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));
并针对此模拟运行测试,因为您取出/模拟的唯一函数是“ c()”。
引用“实用单元测试”这本书:
“总的来说,您不想为了测试而破坏任何封装(或者就像妈妈曾经说过的那样,“不要公开您的私人信息!”)。大多数时候,您应该能够测试一门课程通过行使其公开方法。如果在私有访问或受保护的访问后面隐藏着重要的功能,则可能是一个警告信号,表明其中还有另一个类别正在努力摆脱困境。”
您可以测试私有方法, 但不能模拟(模拟)此方法的运行。
此外,反射不允许您将私有方法转换为受保护的方法或公共方法。setAccessible仅允许您调用原始方法。
另外,您可以使用runkit重命名私有方法并包括“新实现”。但是,这些功能是实验性的,不建议使用它们。
c
是私有的,因此不能在子类中覆盖它。
您可以使用反射,并setAccessible()
在测试中允许您设置对象的内部状态,以使其从私有方法中返回所需的内容。您需要使用PHP 5.3.2。
$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...
您可以模拟受保护的方法,因此,如果可以将C转换为protected,那么此代码将有所帮助。
$mock = $this->getMockBuilder('A')
->disableOriginalConstructor()
->setMethods(array('C'))
->getMock();
$response = $mock->B();
这肯定会工作,它为我工作。然后,为了覆盖受保护的方法C,可以使用反射类。
假设您需要测试$ myClass-> privateMethodX($ arg1,$ arg2),则可以通过反射进行此操作:
$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
这是可以用于在一行中进行此类调用的其他答案的变体:
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);
}
我为我的案例设计了这个通用类:
/**
* @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类中调用的属性/方法来提高性能。
一种选择是使makec()
protected
而不是private
然后继承和覆盖c()
。然后使用您的子类进行测试。另一个选择是将其重构c()
为可以注入的其他类A
(这称为依赖项注入)。然后c()
在单元测试中注入带有模拟实现的测试实例。
另一种解决方案是将您的私有方法更改为受保护的方法,然后进行模拟。
$myMockObject = $this->getMockBuilder('MyMockClass')
->setMethods(array('__construct'))
->setConstructorArgs(array("someValue", 5))
->setMethods(array('myProtectedMethod'))
->getMock();
$response = $myMockObject->myPublicMethod();
在哪里myPublicMethod
打电话myProtectedMethod
。不幸的是,我们无法使用私有方法执行此操作,因为setMethods
无法在可以找到受保护方法的地方找到私有方法