56

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

Here is an idiotic example:

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

So I wanna inject the p value which is protected, so I can't. Should I define setter or IoC, or I can do this with phpunit?

inf3rno
  • 24,976
  • 11
  • 115
  • 197
  • 3
    Just for the record - if you are testing non-public API, then you are doing it wrong. Unit testing is about testing behaviour, not internal implementation. – Mike Doe Jun 24 '15 at 11:02
  • @emix I read that the exact scenario was, that I instantiated other classes in the constructor based on injected values and I did not need them to test the "blah()" method or I just wanted to mock them out. Some people claim that all of these should have been injected, I am still not convinced that the code must change only to be more testable. Though I no longer do unit test, just integration tests, so I no longer care about this. Unit tests are good for the bank sector where bugs can have serious effect. Maybe even there it is too much work to rewrite them by refactoring the code. – inf3rno Nov 10 '22 at 12:32
  • 1
    Let's agree to disagree, but you can write your own code however you wish. – Mike Doe Nov 10 '22 at 12:47
  • @emix Same here. Cheers! – inf3rno Nov 10 '22 at 13:16
  • Not sure why I needed this. Looks like some nasty hacking to put the instance into a certain state from outside. I think it is a very rare scenario and I wanted to spare code with it, which puts it in the actual state. I doubt it is for unit testing, I guesss it is for integration tests with outside stuff like mail client, db client, etc., or if it is for unit testing, then it is totally wrong to do it this way and the constructor was badly designed and probably some of the code should have been moved to a factory or container from it. – inf3rno Feb 23 '23 at 09:50

6 Answers6

68

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.

Dharman
  • 30,962
  • 25
  • 85
  • 135
gontrollez
  • 6,372
  • 2
  • 28
  • 36
  • 2
    Class A is not fully dependency injected, I create new instance of several classes in it's constructor... So I need to override the constructor to mock out those instances. Not the best approach, I think I'll use dependency injection container instead of this. – inf3rno Sep 02 '13 at 16:11
  • your code will be definitely more testable. There are several choices for implementing DI, but this is a really simple one: http://pimple.sensiolabs.org/ – gontrollez Sep 02 '13 at 19:44
  • 5
    Don't use the dependency injection container in the test! A good unit test only tests one class, and ALL dependencies are injected as fully configured mocks. If you cannot do this, you have a bad architecture which should be improved. – Sven Sep 02 '13 at 22:20
  • 1
    instead of `$a->p = 2;`, I had to use `$reflection_property->setValue($a, 2)`, because first approach returned error `Fatal error: Cannot access protected property Store_Api_Version_2_Resource_Products::$_fields in ...` – petrkotek Dec 16 '13 at 00:16
27

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);
}
rsahai91
  • 437
  • 4
  • 13
3

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);
Freefri
  • 698
  • 7
  • 21
2

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'
]);
Oli Girling
  • 605
  • 8
  • 14
1

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

You may want to use the ReflectionProperty as well.

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