DEV Community

Matthew Daly
Matthew Daly

Posted on • Originally published at matthewdaly.co.uk on

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (6)

Collapse
 
curtisfenner profile image
Curtis Fenner

Needing to write private state in tests generally means you have not designed a testable interface (which generally means it's not a very good interface).

You haven't shared an example of why you actually need to have control over id. A reasonable approach would be to just allow the library to set it however, and make sure you get the right answer. (E.g., instead of expecting 'id', expect original->getId()), á la property based testing.

Collapse
 
matthewbdaly profile image
Matthew Daly • Edited

This particular application I did this for is a micro-CMS with support for multiple data sources. The default one uses Markdown files, but I've also written one that uses Doctrine as a backend, which is where this issue arose.

As it's read only (the idea is that the application, at least right now, has no capability to edit content directly, only to pull it from a source), there will never be the need to set many of the fields, including the ID, but at the same time the object's methods should be tested (arguably getters and setters are simple enough that they aren't always worth it, but if there are other methods that depend on those values they probably are worth testing). Given those circumstances, I don't want to add a setter that shouldn't be used in the application at all, but at the same time I want to be able to test it properly.

As I said elsewhere, this is very much an edge case and doesn't arise very often, but it can be a useful technique under certain circumstances.

Another use case that occurs to me would be for domain objects based on views rather than normal database tables. These by definition couldn't be written to and you wouldn't want to implement setters just for testing purposes, but you could easily have a need to test a complex method that built a response based on one or more properties, such as a method to get a URL for the domain object.

Collapse
 
vlasales profile image
Vlastimil Pospichal

Use mocks instead.

Collapse
 
matthewbdaly profile image
Matthew Daly

Mocks aren't useful in this case.

This particular use case was a test for a read-only Doctrine entity. The ID is something that would never be set manually, only by getting it back from a Doctrine repository. As such, there was no reason to mock anything, and no reason to create a setter method just for test purposes.

To be fair, this is very much an edge case, though.

Collapse
 
vlasales profile image
Vlastimil Pospichal

How do you get the property name? It is private and may change at any time in subsequent versions.

Thread Thread
 
matthewbdaly profile image
Matthew Daly

Well, this is my own project I'm testing. If the property name does change in future either I'll know and will remember to update it in the test, or I'll forget and the tests will break in Travis CI, reminding me to fix them. Given that this is a Doctrine entity and so it expects the property in question to be called $id, that's fairly unlikely.