使用PHPUnit模拟私有方法

时间:2011-05-09 13:53:18

标签: php mocking phpunit

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

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

  private function c(){ 
    // some code
  }
}

如何存储私有方法的结果以测试公共函数的更多代码部分。

解决部分阅读here

11 个答案:

答案 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()”。


引用“实用单位测试”一书:

  

“一般情况下,你不想为了测试而打破任何封装(或者像妈妈常说的那样,”不要暴露你的私人!“)。大多数时候,你应该能够通过行使其公共方法来测试一个类。如果隐藏在私人或受保护访问之后的重要功能,那可能是一个警告信号,那里有另一个班级在努力逃脱。“


更多:Why you don't want test private methods.

答案 1 :(得分:25)

你可以test private methods 但你不能模拟(模拟)这种方法的运行。

此外,反射不允许您将私有方法转换为受保护或公共方法。 setAccessible仅允许您调用原始方法。

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

答案 2 :(得分:24)

您可以在测试中使用reflectionsetAccessible(),以便您可以设置对象的内部状态,使其从私有方法返回您想要的内容。你需要使用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() 方法。