用PHPUnit模拟私有方法


73

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

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

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

我该如何对私有方法的结果进行存根以测试公共函数的更多代码部分。

在这里解决了部分阅读

Answers:


92

通常,您只是不直接测试或嘲笑私有和受保护的方法。

您要测试的是您的类的公共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()”。


引用“实用单元测试”这本书:

“总的来说,您不想为了测试而破坏任何封装(或者就像妈妈曾经说过的那样,“不要公开您的私人信息!”)。大多数时候,您应该能够测试一门课程通过行使其公开方法。如果在私有访问或受保护的访问后面隐藏着重要的功能,则可能是一个警告信号,表明其中还有另一个类别正在努力摆脱困境。”


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


这是不正确的,在我的示例中,我需要测试第二个“某些代码...”,但是该结果可能会被我无法嘲笑的私有函数的先前结果所改变,这是示例:public function b() {//一些代码if($ this-> c()== 0)//做一些事情;其他//做其他事情//一些代码}
Tony,

1
@dyoser:如果您确实需要模拟私有方法,则暗示您,结果可能会以您无法通过可访问方法控制的方式更改。或者:如果测试用例(黑盒测试或灰盒测试)需要了解有关要测试的单元内部结构的某些知识,则表明该单元有问题。
KingCrunch

@dyoser当然,私有函数的作用可能会改变它,这就是为什么您通常需要多个测试用例的原因。函数c()产生的每种可能的效果都将由您向类中传递一些参数来触发,因此,如果您传递正确的东西c()也将做正确的事情。无论如何,我将构建一个小示例,您可能想模拟c()
edorian 2011年

2
是的,这就是我对“ //可能设置了受保护的功能以使其正常工作”的意思。以太将功能更改为受保护或使用反射对其进行更改
edorian 2011年

1
“您要测试的是您的类的公共API。其他所有内容都是您的类的实现细节,如果更改它,则不应“破坏”您的测试。” 不应该不代表它不会中断,在这种情况下,我希望我的单元测试指出哪个私有方法可能导致失败。如果仅测试依赖于许多较小单元的公共接口,那么它不是有效的集成测试吗?我认为测试领域陷入了由测试覆盖率决定的怪异公司规则中,在这种情况下,测试公共方法有助于建立良好的统计数据。
NeverEndingQueue

27

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

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

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


扩展A类并实现新的c方法怎么样?
彼得·佩勒

@PetrPeller-由于c是私有的,因此不能在子类中覆盖它。
David Harkness

25

您可以使用反射,setAccessible()在测试中允许您设置对象的内部状态,以使其从私有方法中返回所需的内容。您需要使用PHP 5.3.2。

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

3
不,您正在对属性使用反射,但在示例y中使用了专用方法。只是为了澄清我想模拟(模拟)私有方法的运行以获得我想要的结果(或只是一个模拟结果)。我不需要测试。
托尼

1
@dyoser-如果您不想更改被测类,则必须更改其状态,以使私有方法可以根据需要运行。由于public方法可以调用private方法,并且您不能覆盖private方法,因此您不能对其进行模拟。
David Harkness,

1
为我解决了一个不同的问题,但是却很有魅力。
Kick_the_BUCKET

16

您可以模拟受保护的方法,因此,如果可以将C转换为protected,那么此代码将有所帮助。

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

    $response = $mock->B();

这肯定会工作,它为我工作。然后,为了覆盖受保护的方法C,可以使用反射类。


它确实有效。这是更完整的示例:phpunit.de/manual/current/en/…–
mnv

1
您不应将其更改为无效的设置,例如将私有方法设置为仅受保护以支持测试。如果测试弄乱了您的代码,那就错了。
jgmjgm

12

假设您需要测试$ myClass-> privateMethodX($ arg1,$ arg2),则可以通过反射进行此操作:

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

@爱德森,您好,您的答案与我在链接“测试专用方法”中所说的相同。
doctore 2011年

1
OP不需要测试私有方法。他们需要模拟它以提供罐装结果,同时测试其他方法。
David Harkness 2014年

@DavidHarkness我同意您的观点,即不应测试私有方法,但这是有争议的。这是个阴凉的地方。如果您想在覆盖范围内进行肛门检查,那周围就不会有什么麻烦了。
埃德森·麦地那2014年

@EdsonMedina虽然我确实说过“不需要测试”,但我并不是要暗示OP不应测试私有方法-只是出于这个问题的目的,他们不想这样做。我只是注意到标题根本与正文不符,现在将对其进行修复。
David Harkness 2014年

@DavidHarkness在获得响应并关闭线索后三年更改问题有点为时已晚,您不觉得吗?这会使它变得混乱。
Edson Medina

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

感谢您的摘录。这节省了我很多工作!
Hackbard

7

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

/**
 * @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类中调用的属性/方法来提高性能。


这实际上非常有用,谢谢!你有这对packagist或一些库可能被包含在作曲家?我确实找到了同名的雄辩者/解放者,但我认为这只是一个巧合。
杰夫·普基特

谢谢!不我没有。这是一个巧合。该代码是独立的,因此如果它给您带来了冲突,请随时更改类名。
Torge

6

一种选择是使makec() protected而不是private然后继承和覆盖c()。然后使用您的子类进行测试。另一个选择是将其重构c()为可以注入的其他类A(这称为依赖项注入)。然后c()在单元测试中注入带有模拟实现的测试实例。


当然,但这意味着我需要重写原始类,这不是测试功能的良好实现。
托尼

4
@dyoser:是的,这是必要的。您的原始实现有些无法测试,需要进行一些重构以使其受到测试。不要害怕这样做。迈克尔·费瑟斯(Michael Feathers)在其出色的著作《有效地使用旧版代码》中谈到了这个问题。
Asaph

1

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

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

$response = $myMockObject->myPublicMethod();

在哪里myPublicMethod打电话myProtectedMethod。不幸的是,我们无法使用私有方法执行此操作,因为setMethods无法在可以找到受保护方法的地方找到私有方法


0

您可以使用PHP 7使用匿名类。

$mock = new class Concrete {
    private function bob():void
    {
    }
};

在早期版本的PHP中,您可以创建一个测试类来扩展基类。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.