Mocking MongoCollection with Mockery

I have played around with the mongo extension to PHP lately and came across an issue when I was trying to write unit tests with PHPUnit for some methods using MongoCollection::insert().

Consider a class method looking like this:

class Database {
    public function insertData($hash, Image $image) {
        $data = array(
            'hash' => $hash,
            'name' => $image->getName(),
        );

        $this->getCollection()->insert($data, array('safe' => true));
        $image->setId((string) $data['_id']);

        return true;
    }

    // ...
}

The Image class is just some custom class, and the getCollection() method returns a property set in the class (so I can inject mocked versions of the collection and not depend on MongoCollection).

As you see from the code above the insertData() method changes the first parameter (it adds an _id element that is set to an instance of MongoId). The problem with testing the above method is that when mocking MongoCollection you can’t make it add the _id element to the passed array, so the test fails because of an E_NOTICE that says:

Undefined index: _id.

I tried to solve the problem by using Mockery (a simple but flexible (and awesome) mock object framework by Pádraic Brady) like this:

public function testInsertData() {
    $id = MongoId();
    $idAsString = (string) $id;
    $image = \Mockery::mock('Image');
    $image->shouldReceive('setId')
          ->once()
          ->with($idAsString)
          ->andReturn($image);
    $image->shouldReceive('getName')
          ->once()
          ->andReturn('some name');

    $collection = \Mockery::mock('MongoCollection');
    $collection->shouldReceive('insert')
               ->once()
               ->with(\Mockery::on(
                   function(&$data) use($id) {
                       $data['_id'] = $id;
                       return true;
                   }), \Mockery::type('array'));

    // The object we want to test with
    $database = new Database;
    $database->setCollection($collection);
    $database->insertData($hash, $image);
}

When running the above test I get the following error:

Parameter 1 to {closure}() expected to be a reference, value given

This is because Mockery uses the call_user_func_array function to call the closure given to \Mockery::on(), and that function does not support references. PHPUnit also uses this function internally for its mock objects.

The description of MongoCollection::insert() in the PHP manual looks like this:

public mixed MongoCollection::insert ( array $a [, array $options = array() ] )

Feeling a bit bold I tried to change the insertData() method above to pass an instance of stdClass instead of an array to MongoCollection::insert(). Why? Objects in PHP-5 are all references, so I might be able to get Mockery to work without having to specify &$data in the closure. It’s not documented, but hey, you never know without trying right? Here is the new version of insertData():

public function insertData($hash, Image $image) {
    $data = new stdClass;
    $data->hash = $hash;
    $data->name = $image->getName();

    $this->getCollection()->insert($data, array('safe' => true));
    $image->setId((string) $data->_id);

    return true;
}

The only change I made in the test method was to change the closure I give to \Mockery::on() from:

function(&$data) use ($id) {
    $data['_id'] = $id;
    return true;
}

to

function($data) use ($id) {
    $data->_id = $id;
    return true;
}

and voila, the test works! The part about giving an instance of stdClass to MongoCollection::insert() is not documented, and I’m not sure if it’s a feature that’s going to stay implemented.

At first I tried to solve this using the built in mock objects in PHPUnit, but since they clone the objects used in the mocks the trick above would not work anyway as the change would not be made on the correct reference.

And thats that!

Advertisements
This entry was posted in PHP, Technology and tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s