Testing Your Privates

Sebastian Bergmann » 09 February 2010 » in Articles » 15 Comments

This article is part of a series on testing untestable code:

No, not those privates. If you need help with those, this book might help.

One question I get over and over again when talking about Unit Testing is this:

"How do I test the private attributes and methods of my objects?"

Lets assume we have a class Foo:

<?php
class Foo
{
    private $bar = 'baz';
 
    public function doSomething()
    {
        return $this->bar = $this->doSomethingPrivate();
    }
 
    private function doSomethingPrivate()
    {
        return 'blah';
    }
}
?>

Before we explore how protected and private attributes and methods can be tested directly, lets have a look at how they can be tested indirectly.

The following test calls the testDoSomething() method which in turn calls the doSomethingPrivate() method:

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    /**
     * @covers Foo::doSomething
     * @covers Foo::doSomethingPrivate
     */
    public function testDoSomething()
    {
        $foo = new Foo;
        $this->assertEquals('blah', $foo->doSomething());
    }
}
?>

The test above assumes that testDoSomething() only works correctly when testDoSomethingPrivate() works correctly. This means that we have indirectly tested testDoSomethingPrivate(). The problem with this approach is that when the test fails we do not know directly where the root cause for the failure is. It could be in either testDoSomething() or testDoSomethingPrivate(). This makes the test less valuable.

PHPUnit supports reading protected and private attributes through the PHPUnit_Framework_Assert::readAttribute() method. Convenience wrappers such as PHPUnit_Framework_TestCase::assertAttributeEquals() exist to express assertions on protected and private attributes:

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testPrivateAttribute()
    {
        $this->assertAttributeEquals(
          'baz',  /* expected value */
          'bar',  /* attribute name */
          new Foo /* object         */
        );
    }
}
?>

PHP 5.3.2 introduces the ReflectionMethod::setAccessible() method to allow the invocation of protected and private methods through the Reflection API:

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    /**
     * @covers Foo::doSomethingPrivate
     */
    public function testPrivateMethod()
    {
        $method = new ReflectionMethod(
          'Foo', 'doSomethingPrivate'
        );
 
        $method->setAccessible(TRUE);
 
        $this->assertEquals(
          'blah', $method->invoke(new Foo)
        );
    }
}
?>

In the test above we directly test testDoSomethingPrivate(). When it fails we immediately know where to look for the root cause.

I agree with Dave Thomas and Andy Hunt, who write in their book "Pragmatic Unit Testing":

"In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods. If there is significant functionality that is hidden behind private or protected access, that might be a warning sign that there's another class in there struggling to get out."

So: Just because the testing of protected and private attributes and methods is possible does not mean that this is a "good thing".

Defined tags for this entry: , ,

Freezing and Thawing PHP Objects

Sebastian Bergmann » 29 November 2008 » in Articles, New Features » 12 Comments

One of the many new features that have been added for PHP 5.3 is the setAccessible() method of the ReflectionProperty class that is part of PHP's Reflection API. This method makes protected and private attributes (unfortunately, the class is called ReflectionProperty instead of ReflectionAttribute) of a class or object accessible for the ReflectionProperty::getValue() and ReflectionProperty::setValue() methods, thus making protected and private attributes "open" for full read and write access from the outside.

Among other use cases, this addition to PHP's meta programming capabilities makes the customized serialization of objects possible as illustrated by the following proof-of-concept implementation:

<?php
class Object_Freezer
{
    public static function freeze($object)
    {
        $state     = array();
        $reflector = new ReflectionObject($object);
 
        foreach ($reflector->getProperties() as $attribute) {
            $attribute->setAccessible(TRUE);
            $state[$attribute->getName()] =
            $attribute->getValue($object);
        }
 
        return array(
          'className' => get_class($object), 'state' => $state
        );
    }
 
    public static function thaw(array $frozenObject)
    {
        if (!class_exists($frozenObject['className'])) {
            throw new RuntimeException(
              sprintf(
                'Class "%s" could not be found.',
                $frozenObject['className']
              )
            );
        }
 
        // Use a "trick" to create an object of the class
        // without calling its constructor. After all, we
        // are not creating a new object but are merely
        // thawing a previously created and currently
        // frozen one.
        $object = unserialize(
          sprintf(
            'O:%d:"%s":0:{}',
            strlen($frozenObject['className']),
            $frozenObject['className']
          )
        );
 
        $reflector = new ReflectionObject($object);
 
        foreach ($frozenObject['state'] as $name => $value) {
            $attribute = $reflector->getProperty($name);
            $attribute->setAccessible(TRUE);
            $attribute->setValue($object, $value);
        }
 
        return $object;
    }
}
?>

The following snippet of code uses the Object_Freezer class (see above) to "freeze" and "thaw" an object:

<?php
require 'Object/Freezer.php';
 
class Foo
{
    public $a;
    protected $b;
    private $c;
 
    public function __construct($a, $b, $c)
    {
        $this->a = $a;
        $this->b = $b;
        $this->c = $c;
    }
}
 
$object = new Foo(1, 2, 3);
var_dump($object);
 
$frozenObject = Object_Freezer::freeze($object);
var_dump($frozenObject);
 
$object = Object_Freezer::thaw($frozenObject);
var_dump($object);
?>

Below is the output produced by the script above:

object(Foo)#1 (3) {
  ["a"]=>
  int(1)
  ["b":protected]=>
  int(2)
  ["c":"Foo":private]=>
  int(3)
}
array(2) {
  ["className"]=>
  string(3) "Foo"
  ["state"]=>
  array(3) {
    ["a"]=>
    int(1)
    ["b"]=>
    int(2)
    ["c"]=>
    int(3)
  }
}
object(Foo)#5 (3) {
  ["a"]=>
  int(1)
  ["b":protected]=>
  int(2)
  ["c":"Foo":private]=>
  int(3)
}

The code for the Object_Freezer class (see above) was written by Stefan Priebsch and myself somewhere over the Atlantic ocean during our flight from Frankfurt to Atlanta for the php|works conference earlier this month. However, only as of today (patch) does it actually work.

Update: The code for Object_Freezer is now available on GitHub.

Defined tags for this entry: , ,

Reflection_Annotation in CVS

Sebastian Bergmann » 23 May 2005 » in PHP » 0 Comments

My Proposal for "Reflection_Annotation" has been accepted and I committed an initial development version to the PEAR CVS Repository.
Defined tags for this entry: , ,

Annotations in PHP

Sebastian Bergmann » 19 April 2005 » in PHP » 4 Comments

Yesterday someone proposed adding support for annotations to the PHP Interpreter. Although it would be nice to have this kind of functionality out-of-the-box and built into the interpreter itself this is not necessary.

I could not resist and started working on a package that exends the Reflection API of PHP 5 by adding getAnnotations(), getAnnotation($name), and hasAnnotation($name) methods to ReflectionClass, ReflectionFunction, ReflectionMethod, and ReflectionProperty.

The syntax for annotations follows the standard for PHP Doc Comments and @foo bar declares an annotation of name "foo" with value "bar".

The inital code can be found here:A usage example can be found here.
Defined tags for this entry: , ,