Matthew Daly's Blog

I'm a web developer in Norfolk. This is my blog...

7th September 2019 8:16 pm

Setting Private Properties in Tests

Sometimes when writing a test, you come across a situation where you need to set a private field that’s not accessible through any existing route. For instance, I’ve been working with Doctrine a bit lately, and since the ID on an entity is generated automatically, it should not be possible to change it via a setter, but at the same time, we sometimes have the need to set it in a test.

Fortunately, there is a way to do that. Using PHP’s reflection API, you can temporarily mark a property on an object as accessible, so as to be able to set it without either passing it to the constructor or creating a setter method that will only ever be used by the test. We first create a ReflectionClass instance from the object, then get the property. We mark it as accessible, and then set the value on the instance, as shown below:

<?php declare(strict_types = 1);

namespace Tests\Unit;

use Tests\TestCase;
use Project\Document;
use ReflectionClass;

final class DocumentTest extends TestCase
{
    public function testGetId()
    {
        $doc = new Document();
        $reflect = new ReflectionClass($doc);
        $id = $reflect->getProperty('id');
        $id->setAccessible(true);
        $id->setValue($doc, 1);
        $this->assertEquals(1, $doc->getId());
    }
}

If you’re likely to need this in more than one place, you may want to pull this functionality out into a trait for reuse:

<?php declare(strict_types = 1);

namespace Tests\Traits;

use ReflectionClass;

trait SetsPrivateProperties
{
    /**
     * Sets a private property
     *
     * @param mixed $object
     * @param string $property
     * @param mixed $value
     * @return void
     */
    public function setPrivateProperty($object, string $property, $value)
    {
        $reflect = new ReflectionClass($object);
        $prop = $reflect->getProperty($property);
        $prop->setAccessible(true);
        $prop->setValue($object, $value);
        $prop->setAccessible(false);
    }
}

Then your test can be simplified as follows:

<?php declare(strict_types = 1);

namespace Tests\Unit;

use Tests\TestCase;
use Project\Document;
use Tests\Traits\SetsPrivateProperties;

final class DocumentTest extends TestCase
{
    use SetsPrivateProperties;

    public function testGetId()
    {
        $doc = new Document();
        $this->setPrivateProperty($doc, 'id', 1);
        $this->assertEquals(1, $doc->getId());
    }
}

While this is a slightly contrived and limited example, and this situation is quite rare, I’ve found it to be a useful technique under certain circumstances.

View the article with comments
Skipping Environment Specific Phpunit Tests