mockbuilder-设置mock对象的内部属性


mockbuilder - set mock object internal property

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

这里有一个愚蠢的例子:

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来做这件事?

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

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

无论如何,在您的示例中,您不需要为要引发的Exception设置p值。您正在使用mock来控制对象行为,而不考虑其内部。

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

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

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

我想留下一个方便的助手方法,可以快速复制粘贴到这里:

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

根据@gontrollerz接受的答案,由于我们使用的是模拟生成器,因此我们不需要调用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);

基于上面的@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'
]);

如果每个代码库都使用DI和IoC,并且从未做过这样的事情,那将是令人惊讶的:

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

当然,你可以在构造函数中使用mock BlahClass,但构造函数会将一个受保护的属性设置为你不能模拟的属性。

所以你可能在想"好吧,重构构造函数,用FooClass代替BlahClass,那么你就不必在构造函数中实例化FooClass,你可以放一个mock!"好吧,你是对的,如果这不意味着你必须改变整个代码库中类的每一种用法,给它一个FooClass而不是BlahClass。

并不是每个代码库都是完美的,有时你只需要完成一些事情。这意味着,是的,有时你需要打破"只测试公共API"的规则。

您可能还需要使用ReflectionProperty

$reflection = new ReflectionProperty(Foo::class, 'myProperty');
$reflection->setAccessible(true); // Do this if less than PHP8.1.
$reflection->setValue($yourMock, 'theValue');