51

是否可以使用禁用的构造函数和手动设置的受保护属性创建模拟对象?

这是一个愚蠢的例子:

class A {
    protected $p;
    public function __construct(){
        $this->p = 1;
    }

    public function blah(){
        if ($this->p == 2)
            throw Exception();
    }
}

class ATest extend bla_TestCase {
    /** 
        @expectedException Exception
    */
    public function testBlahShouldThrowExceptionBy2PValue(){
        $mockA = $this->getMockBuilder('A')
            ->disableOriginalConstructor()
            ->getMock();
        $mockA->p=2; //this won't work because p is protected, how to inject the p value?
        $mockA->blah();
    }
}

所以我想注入受保护的 p 值,所以我不能。我应该定义 setter 还是 IoC,或者我可以用 phpunit 来做到这一点?

4

5 回答 5

66

您可以使用反射使属性公开,然后设置所需的值:

$a = new A;
$reflection = new ReflectionClass($a);
$reflection_property = $reflection->getProperty('p');
$reflection_property->setAccessible(true);

$reflection_property->setValue($a, 2);

无论如何,在您的示例中,您不需要为要引发的异常设置 p 值。您正在使用一个模拟来控制对象的行为,而不考虑它的内部。

因此,不是设置 p = 2 来引发异常,而是将模拟配置为在调用 blah 方法时引发异常:

$mockA = $this->getMockBuilder('A')
        ->disableOriginalConstructor()
        ->getMock();
$mockA->expects($this->any())
         ->method('blah')
         ->will($this->throwException(new Exception));

最后,奇怪的是你在 ATest 中嘲笑 A 类。您通常模拟您正在测试的对象所需的依赖项。

希望这可以帮助。

于 2013-09-01T20:54:41.977 回答
26

以为我会留下一个方便的帮助方法,可以在这里快速复制和粘贴:

/**
 * Sets a protected property on a given object via reflection
 *
 * @param $object - instance in which protected value is being modified
 * @param $property - property on instance being modified
 * @param $value - new value of the property being modified
 *
 * @return void
 */
public function setProtectedProperty($object, $property, $value)
{
    $reflection = new ReflectionClass($object);
    $reflection_property = $reflection->getProperty($property);
    $reflection_property->setAccessible(true);
    $reflection_property->setValue($object, $value);
}
于 2016-06-06T21:16:39.397 回答
2

根据@gontrollez 接受的答案,由于我们使用的是模拟构建器,因此我们不需要调用new A;,因为我们可以使用类名。

    $a = $this->getMockBuilder(A::class)
        ->disableOriginalConstructor()
        ->getMock();

    $reflection = new ReflectionClass(A::class);
    $reflection_property = $reflection->getProperty('p');
    $reflection_property->setAccessible(true);

    $reflection_property->setValue($a, 2);
于 2020-04-15T10:01:37.983 回答
1

如果每个代码库都使用 DI 和 IoC,并且从未做过这样的事情,那就太棒了:

public function __construct(BlahClass $blah)
{
    $this->protectedProperty = new FooClass($blah);
}

当然,您可以在构造函数中使用模拟 BlahClass,但随后构造函数将受保护的属性设置为您无法模拟的内容。

因此,您可能在想“重构构造函数以采用 FooClass 而不是 BlahClass,那么您不必在构造函数中实例化 FooClass,而是可以放入一个模拟!” 好吧,你是对的,如果这并不意味着你必须改变整个代码库中类的每一次使用,给它一个 FooClass 而不是一个 BlahClass。

并非每个代码库都是完美的,有时您只需要完成工作即可。这意味着,是的,有时您需要打破“仅测试公共 API”的规则。

于 2016-09-20T22:02:52.890 回答
0

基于上面的@rsahai91 答案,创建了一个新的帮助程序来使多种方法可以访问。可以是私有的或受保护的

/**
 * Makes any properties (private/protected etc) accessible on a given object via reflection
 *
 * @param $object - instance in which properties are being modified
 * @param array $properties - associative array ['propertyName' => 'propertyValue']
 * @return void
 * @throws ReflectionException
 */
public function setProperties($object, $properties)
{
    $reflection = new ReflectionClass($object);
    foreach ($properties as $name => $value) {
        $reflection_property = $reflection->getProperty($name);
        $reflection_property->setAccessible(true);
        $reflection_property->setValue($object, $value);
    }
}

示例使用:

$mock = $this->createMock(MyClass::class);

$this->setProperties($mock, [
    'propname1' => 'valueOfPrivateProp1',
    'propname2' => 'valueOfPrivateProp2'
]);
于 2020-03-17T20:08:52.547 回答