RFC: Mocking protected methods

If you enjoyed this article, please leave a comment, rss subscribe to my RSS feed and/or follow me on Twitter. Thank you very much!

Update, 2011-06-16, 12:15 AM Thanks for the comments.

(I swear I had something like that before and it didn't work!) Here's the solution:

$userId = 61382;
$docId  = 'CLD2_62e029fc-1dae-4f20-873e-69facb64a21a';
$body   = '{"error":"missing","reason":"problem?"}';

$client = new Zend_Http_Client;
$client->setAdapter(new Zend_Http_Client_Adapter_Test);

$couchdb = $this->getMock(
    'CouchDB',
    array('makeRequest',),
    array($userId, $this->config,)
);
$couchdb->expects($this->once())
    ->method('makeRequest')
    ->will($this->returnValue(new Zend_Http_Response(200, array(), $body)));
$couchdb->setHttpClient($client);
$couchdb->getDocument($docId);

--- Original blog entry ---

I wrote a couple tests for a small CouchDB access wrapper today. But when I wrote the implementation itself, I realized that my class setup depends on an actual CouchDB server being available and here my journey began.

Example code

Consider the following example:

My objective is not to be able to test any of the protected methods directly, but to be able to supply a fixture so we don't have to setup CouchDB to run our testsuite. My fixture would replace makeRequest() and return a JSON string instead.

Testing your privates?

So this was my first encounter with PHPUnit's mock feature. I found it a little confusing because by definition $this->getMock() (or $this->getMockBuilder('CouchDB')->getMock()), creates stubs and not mock objects as the name suggests.

Of course it's still possible to configure an expectations to create actual mock objects, so maybe (!) I'm being pedantic here.

In the process I read Sebastian's blog entry Testing Your Privates, but it didn't help me either since I didn't want to test makeRequest() directly.

The biggest problem seems to be that PHPUnit's mock feature cannot setup protected methods. The mockable methods have to be public.

Solution

I realize my class layout might be flawed (which is why I titled this RFC), but my current solution looks simplyfied like the following. In Ruby-land, this is called a fake:

class CouchDBMock extends CouchDB
{
    protected function makeRequest($uri)
    {
        return '{"error":"missing","reason":"problem?"}';
    }
}

My test setup is as follows:

class CouchDBTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException RuntimeException
     */
    public function testDocumentNotFound()
    {
        $couchdb = new CouchDBMock;
        $couchdb->getDocument('random');
    }
}

Fin

Thoughts, anyone? I probably spent too much time thinking about this.

I made my class code an github gist, so if I made any obvious mistakes, just fork away.

| More