使用PHPUnit模拟私有方法

Ton*_*ony 66 php phpunit mocking

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

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

  private function c(){ 
    // some code
  }
}
Run Code Online (Sandbox Code Playgroud)

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

解决了部分阅读在这里

edo*_*ian 81

通常,您不会直接测试或模拟私有和受保护的方法.

您要测试的是您班级的公共 API.其他所有内容都是您班级的实施细节,如果您更改了测试,则不应"破坏"您的测试.

当您发现"无法获得100%的代码覆盖率"时,这也会对您有所帮助,因为您的类中可能存在无法通过调用公共API执行的代码.


你通常不想这样做

但如果你的班级看起来像这样:

class a {

    public function b() {
        return 5 + $this->c();
    }

    private function c() {
        return mt_rand(1,3);
    }
}
Run Code Online (Sandbox Code Playgroud)

我可以看到需要模拟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);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在不再需要模拟"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));
Run Code Online (Sandbox Code Playgroud)

并且针对这个模拟运行测试,因为你取出/ mock的唯一函数是"c()".


引用"实用单元测试"一书:

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


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

  • “你想要测试的是你的类的公共 API。其他一切都是你的类的实现细节,如果你改变它,不应该“破坏”你的测试。” 不应该并不意味着它不会中断,在这种情况下,我希望我的单元测试能够指示哪些私有方法可能导致失败。如果我仅测试依赖于许多较小单元的公共接口,那么它不是有效的集成测试吗?我认为测试世界陷入了由测试覆盖率决定的奇怪的公司规则,在这种情况下测试公共方法有助于建立良好的统计数据。 (3认同)
  • 是的,这就是我所说的"//可能设置保护此功能的功能"评论.以太将功能更改为受保护或使用反射来更改它 (2认同)

doc*_*ore 25

您可以测试私有方法, 但不能模拟(模拟)此方法的运行.

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

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


Dav*_*ess 24

您可以使用反射setAccessible()在测试中允许您以这样的方式设置对象的内部状态,即它将从私有方法返回您想要的内容.你需要使用PHP 5.3.2.

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

  • 不,你在属性上使用反射,但在示例中使用私有方法.只是为了澄清我想模拟(模拟)私有方法的运行以获得我想要的结果(或只是模拟的结果).我不需要测试它. (3认同)

小智 13

你可以模拟受保护的方法,所以如果你可以将C转换为protected,那么这段代码将有所帮助.

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

    $response = $mock->B();
Run Code Online (Sandbox Code Playgroud)

这肯定会奏效,它对我有用.然后,为了覆盖受保护的方法C,您可以使用反射类.


Eds*_*ina 11

假设您需要测试$ myClass-> privateMethodX($ arg1,$ arg2),您可以使用反射执行此操作:

$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
Run Code Online (Sandbox Code Playgroud)


小智 10

以下是其他答案的变体,可用于将此类调用排成一行:

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);
}
Run Code Online (Sandbox Code Playgroud)

  • 问题是关于如何模拟私有方法的调用,而不是关于如何在单元测试中直接调用私有方法。 (2认同)

Tor*_*rge 6

我为我的案例设计了这个通用类:

/**
 * @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);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用此类,您现在可以轻松透明地释放任何对象上的所有私有函数/字段。

$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);
Run Code Online (Sandbox Code Playgroud)

如果性能很重要,则可以通过缓存Liberator类中调用的属性/方法来提高性能。


Asa*_*aph 5

一种选择是c() protected代替,private然后子类和覆盖c().然后用你的子类测试.另一个选择是重构c()为可以注入的不同类A(这称为依赖注入).然后c()在单元测试中注入一个带有模拟实现的测试实例.

  • @dyoser:是的,这是必要的.您的原始实现有点不可测试,需要稍微重构以使其受到测试.不要害怕这样做.Michael Feathers在他出色的着作[有效地使用遗留代码](http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052/)中讨论了这个问题. (4认同)