phpunit - mockbuilder - set mock object internal property

后端 未结 5 903
别那么骄傲
别那么骄傲 2020-12-24 00:55

Is it possible to create a mock object with disabled constructor and manually setted protected properties?

Here is an idiotic example:

class A {
             


        
相关标签:
5条回答
  • 2020-12-24 01:32

    Based on @rsahai91 answer above, created a new helper for making multiple methods accessible. Can be private or protected

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

    Example use:

    $mock = $this->createMock(MyClass::class);
    
    $this->setProperties($mock, [
        'propname1' => 'valueOfPrivateProp1',
        'propname2' => 'valueOfPrivateProp2'
    ]);
    
    0 讨论(0)
  • 2020-12-24 01:38

    You can make the property public by using Reflection, and then set the desired value:

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

    Anyway in your example you don't need to set p value for the Exception to be raised. You are using a mock for being able to take control over the object behaviour, without taking into account it's internals.

    So, instead of setting p = 2 so an Exception is raised, you configure the mock to raise an Exception when the blah method is called:

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

    Last, it's strange that you're mocking the A class in the ATest. You usually mock the dependencies needed by the object you're testing.

    Hope this helps.

    0 讨论(0)
  • 2020-12-24 01:41

    It would be amazing if every codebase used DI and IoC, and never did stuff like this:

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

    You can use a mock BlahClass in the constructor, sure, but then the constructor sets a protected property to something you CAN'T mock.

    So you're probably thinking "Well refactor the constructor to take a FooClass instead of a BlahClass, then you don't have to instantiate the FooClass in the constructor, and you can put in a mock instead!" Well, you'd be right, if that didn't mean you would have to change every usage of the class in the entire codebase to give it a FooClass instead of a BlahClass.

    Not every codebase is perfect, and sometimes you just need to get stuff done. And that means, yes, sometimes you need to break the "only test public APIs" rule.

    0 讨论(0)
  • 2020-12-24 01:44

    Thought i'd leave a handy helper method that could be quickly copy and pasted here:

    /**
     * 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);
    }
    
    0 讨论(0)
  • 2020-12-24 01:54

    Based on the accepted answer from @gontrollez, since we are using a mock builder we do not have the need to call new A; since we can use the class name instead.

        $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);
    
    0 讨论(0)
提交回复
热议问题