Setting private properties in tests

Published by at 7th September 2019 7:16 pm

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:

1<?php declare(strict_types = 1);
2
3namespace Tests\Unit;
4
5use Tests\TestCase;
6use Project\Document;
7use ReflectionClass;
8
9final class DocumentTest extends TestCase
10{
11 public function testGetId()
12 {
13 $doc = new Document();
14 $reflect = new ReflectionClass($doc);
15 $id = $reflect->getProperty('id');
16 $id->setAccessible(true);
17 $id->setValue($doc, 1);
18 $this->assertEquals(1, $doc->getId());
19 }
20}

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:

1<?php declare(strict_types = 1);
2
3namespace Tests\Traits;
4
5use ReflectionClass;
6
7trait SetsPrivateProperties
8{
9 /**
10 * Sets a private property
11 *
12 * @param mixed $object
13 * @param string $property
14 * @param mixed $value
15 * @return void
16 */
17 public function setPrivateProperty($object, string $property, $value)
18 {
19 $reflect = new ReflectionClass($object);
20 $prop = $reflect->getProperty($property);
21 $prop->setAccessible(true);
22 $prop->setValue($object, $value);
23 $prop->setAccessible(false);
24 }
25}

Then your test can be simplified as follows:

1<?php declare(strict_types = 1);
2
3namespace Tests\Unit;
4
5use Tests\TestCase;
6use Project\Document;
7use Tests\Traits\SetsPrivateProperties;
8
9final class DocumentTest extends TestCase
10{
11 use SetsPrivateProperties;
12
13 public function testGetId()
14 {
15 $doc = new Document();
16 $this->setPrivateProperty($doc, 'id', 1);
17 $this->assertEquals(1, $doc->getId());
18 }
19}

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.