Setting private properties in tests
Published by Matthew Daly 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);23namespace Tests\Unit;45use Tests\TestCase;6use Project\Document;7use ReflectionClass;89final class DocumentTest extends TestCase10{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);23namespace Tests\Traits;45use ReflectionClass;67trait SetsPrivateProperties8{9 /**10 * Sets a private property11 *12 * @param mixed $object13 * @param string $property14 * @param mixed $value15 * @return void16 */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);23namespace Tests\Unit;45use Tests\TestCase;6use Project\Document;7use Tests\Traits\SetsPrivateProperties;89final class DocumentTest extends TestCase10{11 use SetsPrivateProperties;1213 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.