phpunit - mockbuilder - 设置模拟对象内部属性

时间:2013-09-01 11:56:47

标签: php phpunit

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

这是一个愚蠢的例子:

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做到这一点?

5 个答案:

答案 0 :(得分:46)

您可以使用Reflection将属性设为公共属性,然后设置所需的值:

$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方法时将mock配置为引发异常:

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

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

希望这有帮助。

答案 1 :(得分:18)

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

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

答案 2 :(得分:0)

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

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

你可以在构造函数中使用模拟BlahClass,但是然后构造函数将受保护的属性设置为你不能模拟的东西。

所以你可能正在考虑“重构构造函数以获取FooClass而不是BlahClass,那么你不必在构造函数中实例化FooClass,而你可以放入模拟代替!”好吧,你是对的,如果这并不意味着你必须改变整个代码库中类的每个用法,给它一个FooClass而不是BlahClass。

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

答案 3 :(得分: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'
]);

答案 4 :(得分:0)

基于@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);