用PHPUnit模拟私有方法

我有一个关于使用PHPUnit模拟一个类中的私有方法的问题。 我举个例子来介绍一下:

class A { public function b() { // some code $this->c(); // some more code } private function c(){ // some code } } 

如何将私有方法的结果存根testing公共函数的更多代码部分。

解决了部分阅读在这里

通常你只是不直接testing或模拟私有和受保护的方法。

你想testing的是你的class级的公共 API。 其他的一切都是你的课程的实现细节,如果你改变了它,不应该“破坏”你的testing。

当您注意到“无法获得100%的代码覆盖率”时,这也会对您有所帮助,因为您的类中可能存在无法通过调用公共API来执行的代码。


你通常不想这样做

但是,如果你的class级看起来像这样:

 class a { public function b() { return 5 + $this->c(); } private function c() { return mt_rand(1,3); } } 

我可以看到需要模拟c(),因为“随机”函数是全局状态,你不能testing。

“干净?/详细?/过度复杂 – 也许?/我喜欢它通常”的解决scheme

 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()”了,因为它不包含任何全局variables,而且可以很好地testing。


如果你不想或者不能从你的私有函数中删除全局状态(不好的事情不好的现实或者坏的定义可能会不同),你可以用这个模拟器testing。

 // 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)); 

并运行你的testing对这个模拟,因为你拿出/模拟的唯一function是“c()”。


引用“实用unit testing”一书:

“一般来说,你不想为了testing而破坏任何封装(或者像妈妈曾经说过的,”不要暴露你的私生子!“)。大多数情况下,你应该能够testing一个类通过行使其公开的方法,如果隐藏在私人或受保护通道后面的重要function,这可能是一个警告信号,说明在那里还有另一个class正在努力挣脱出来。


还有一些: Why you don't want test private methods.

你可以testing私有方法,但是你不能模拟(模拟)这个方法的运行。

此外,reflection不允许您将私有方法转换为受保护或公共方法。 setAccessible只允许你调用原始的方法。

或者,您可以使用runkit来重命名私有方法,并包含一个“新的实现”。 但是,这些function是实验性的,不build议使用它们。

你可以在你的testing中使用reflection和setAccessible()来允许你设置你的对象的内部状态,以便它能从私有方法中返回你想要的内容。 你需要在PHP 5.3.2上。

 $fixture = new MyClass(...); $reflector = new ReflectionProperty('MyClass', 'myPrivateProperty'); $reflector->setAccessible(true); $reflector->setValue($fixture, 'value'); // test $fixture ... 

你可以得到保护方法的模拟,所以如果你可以将C转换成受保护的,那么这个代码将会有所帮助。

  $mock = $this->getMockBuilder('A') ->disableOriginalConstructor() ->setMethods(array('C')) ->getMock(); $response = $mock->B(); 

这一定会工作,它为我工作。 那么对于覆盖受保护的方法C,您可以使用reflection类。

假设你需要testing$ myClass-> privateMethodX($ arg1,$ arg2),你可以用reflection来做到这一点:

 $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); } 

一种select是使c() protected而不是private ,然后子类和重写c() 。 然后testing你的子类。 另一种select是将c()重构为可以注入到A的另一个类(这称为dependency injection)。 然后在你的unit testing中用一个模拟的c()实现注入一个testing实例。

我为我的情况提出了这个通用课程:

 /** * @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); 

如果性能很重要,可以通过cachingLiberator类中调用的属性/方法来改善性能。

另一种解决scheme是将私有方法更改为受保护的方法,然后进行模拟。

 $myMockObject = $this->getMockBuilder('MyMockClass') ->setMethods(array('__construct')) ->setConstructorArgs(array("someValue", 5)) ->setMethods(array('myProtectedMethod')) ->getMock(); $response = $myMockObject->myPublicMethod(); 

myPublicMethod调用myProtectedMethod 。 不幸的是,我们不能用私有方法来做这件事,因为setMethods找不到一个私有方法,因为它可以find一个受保护的方法