使用 PHPUnit 模拟私有方法

发布于 2024-11-06 03:10:54 字数 346 浏览 4 评论 0原文

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

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

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

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

部分阅读此处解决

I have a question about using PHPUnit to mock a private method inside a class. Let me introduce with an example:

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

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

How can I stub the result of the private method to test the some more code part of the public function.

Solved partially reading here

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

云巢 2024-11-13 03:10:54

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

您要测试的是您的类的 public API。其他一切都是您的类的实现细节,如果您更改它,则不应“破坏”您的测试。

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


你通常不想这样做

但是如果你的类看起来像这样:

class a {

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

    private function c() {
        return mt_rand(1,3);
    }
}

我可以看到需要模拟 c() 因为“随机”函数是全局状态并且你无法测试它。

“clean?/verbose?/overcomplicated-maybe?/i-like-it-usually”解决方案

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


引用《实用单元测试》一书:

“一般来说,您不想为了测试而破坏任何封装(或者正如妈妈常说的,“不要暴露您的隐私!”)。大多数时候,您应该能够通过执行类的公共方法来测试类。如果私有或受保护的访问背后隐藏着重要的功能,这可能是一个警告信号,表明其中有另一个类正在努力摆脱它。”


更多:为什么你不这样做不想测试私有方法。

Usually you just don't test or mock the private & protected methods directy.

What you want to test is the public API of your class. Everything else is an implementation detail for your class and should not "break" your tests if you change it.

That also helps you when you notice that you "can't get 100% code coverage" because you might have code in your class that you can't execute by calling the public API.


You usually don't want to do this

But if your class looks like this:

class a {

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

    private function c() {
        return mt_rand(1,3);
    }
}

i can see the need to want to mock out c() since the "random" function is global state and you can't test that.

The "clean?/verbose?/overcomplicated-maybe?/i-like-it-usually" Solution

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

now there is no more need to mock "c()" out since it does not contain any globals and you can test nicely.


If you don't want to do or can't remove the global state from your private function (bad thing bad reality or you definition of bad might be different) that you can test against the mock.

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

and run your tests against this mock since the only function you take out/mock is "c()".


To quote the "Pragmatic Unit Testing" book:

"In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods. If there is significant functionality that is hidden behind private or protected access, that might be a warning sign that there's another class in there struggling to get out."


Some more: Why you don't want test private methods.

幽梦紫曦~ 2024-11-13 03:10:54

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

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

You can use reflection and setAccessible() in your tests to allow you to set the internal state of your object in such a way that it will return what you want from the private method. You'll need to be on PHP 5.3.2.

$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...
回眸一遍 2024-11-13 03:10:54

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

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

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

You can test private methods
but you can't simulate (mock) the running of this methods.

Furthermore, the reflection does not allow you to convert a private method to a protected or public method. setAccessible only allows you to invoke the original method.

Alternatively, you could use runkit for rename the private methods and include a "new implementation". However, these features are experimental and their use is not recommended.

柏林苍穹下 2024-11-13 03:10:54

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

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

    $response = $mock->B();

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

You can get mock of protected method , so if you can convert C to protected then this code will help.

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

    $response = $mock->B();

This will definitely work , It worked for me .
Then For covering protected method C you can use reflection classes.

孤星 2024-11-13 03:10:54

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

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

Assuming that you need to test $myClass->privateMethodX($arg1, $arg2), you can do this with reflection:

$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
命硬 2024-11-13 03:10:54

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

One option would be to make c() protected instead of private and then subclass and override c(). Then test with your subclass. Another option would be to refactor c() out into a different class that you can inject into A (this is called dependency injection). And then inject a testing instance with a mock implementation of c() in your unit test.

初见终念 2024-11-13 03:10:54

以下是其他答案的变体,可用于在一行中进行此类调用:

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

Here's a variation of the other answers that can be used to make such calls one line:

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);
}
菊凝晚露 2024-11-13 03:10:54

我为我的案例想出了这个通用类:

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

I came up with this general purpose class for my case:

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

With this class you can now easily & transparently liberate all private functions/fields on any object.

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

If performance is important, it can be improved by caching the properties/methods called in the Liberator class.

浮世清欢 2024-11-13 03:10:54

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

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

$response = $myMockObject->myPublicMethod();

其中 myPublicMethod 调用 myProtectedMethod。不幸的是,我们不能使用私有方法来做到这一点,因为 setMethods 无法找到私有方法,而它可以找到受保护的方法

An alternative solution is to change your private method to a protected method and then mock.

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

$response = $myMockObject->myPublicMethod();

where myPublicMethod calls myProtectedMethod. Unfortunately we can not do this with private methods since setMethods can not find a private method where as it can find a protected method

江南烟雨〆相思醉 2024-11-13 03:10:54

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

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

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

You can use anonymous classes using PHP 7.

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

In prior versions of PHP you can make a test class extending the base class.

暮年 2024-11-13 03:10:54

当目标类既不是静态的也不是最终的时,我以接近@jgmjgm的方式解决此类问题,通过匿名和本地扩展原始类,因此不在不同的测试用例中多次测试相同的方法:

$extent = new class ($args) extends A {
    private function c() { 
        return $hardCodedValue;
    }
};

然后我可以测试$extent->b() 的行为,而无需再次执行可能繁重的 c() 方法。

When the target class is neither static nor final, I solve this kind of problem in a way close to @jgmjgm, by anonymously and locally extending the original class and therefore not testing the same method multiple times in different test cases :

$extent = new class ($args) extends A {
    private function c() { 
        return $hardCodedValue;
    }
};

Then I can test the behaviour of $extent->b() without executing again the potentially heavy c() method.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文