Mock Object Framework

Discussion of testing theory and practice, including methodologies (such as TDD, BDD, DDD, Agile, XP) and software - anything to do with testing goes here. (Formerly "The Testing Side of Development")

Moderator: General Moderators

Post Reply
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Mock Object Framework

Post by Maugrim_The_Reaper »

The long version is here: http://blog.astrumfutura.com/archives/3 ... -Wins.html

As a few have noticed, my previously suggested project called PHPSpec (BDD for PHP) has being progressing rapidly. Recently in using PHPSpec I verified the one thing it really lacks is Mock Object support. We were aware this was going to be a problem eventually.

Subsequent to the blog post, the idea of PHPMock has become a little more concrete. It will be executed and released as an independent Mock Object (and Stub) framework for PHP, capable of integration with PHPSpec, PHPT, and PHPUnit. SimpleTest will likely follow once it's migrated to PHP5 (afterall two of PHPSpec's developers are also SimpleTesters ;)).

Sebastian Bergmann responded to the blog suggesting a standalone framework would be a good solution. At the moment PHPUnit's stock Mock Objects are fairly difficult to use and lack a few features like indexed return values.

To pass the suggestion by here for comment (where else?), I've written some semblance of PHPMock code along with a set of BDD specs, available from:
http://phpmock.googlecode.com/svn/trunk/ (this is also the subversion url)

The code so far implements an API of the form:

Code: Select all

// create Mock/Stub
$class = PHPMock::mock('Album')->getMockClassName();
$mock = new $class;

// set expectations
$mock->shouldReceive('setName')->once()->with('Highway To Hell');
$mock->shouldReceive('setArtist')->once()->with('AC/DC');
$mock->shouldReceive('getArtist')->times(2)->andReturn('AC/DC');
$mock->shouldReceive('getName')->zeroOrMoreTimes()->andReturn('Highway To Hell', 'Highway To Hell', 'StopAsking!');

// do actual stuff
$mock->setName('Highway To Hell');
$mock->setArtist('AC/DC');
$mock->getArtist(); // AC/DC
$mock->getArtist(); // AC/DC
// last return value is persistent on all subsequent calls
$mock->getName(); // Highway To Hell
$mock->getName(); // Highway To Hell
$mock->getName(); // StopAsking!
$mock->getName(); // StopAsking!
$mock->getName(); // StopAsking!

// check all expectations are verified
$mock->verify(); // throws exception by default
Or a simple Stub generator:

Code: Select all

$class = PHPMock::mock('Album')->getMockClassName();
$stub = new $class;
$stub->shouldReceive( array('getName'=>'Black Album', 'getArtist'=>'Metallica') );

// as before, the LAST return value for a method is persistently returned on all subsequent calls
$stub->getName(); // Black Album
$stub->getArtist(); // Metallica
Are there specific use cases anyone would like to see? Any other comments are of course welcome ;).
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

In Smalltalk, we had one issue of repeated (and ordered) method calling which took a while to round of. The following (paraphrased) code would fail validation, despite being "correct":

Code: Select all

$mock->shouldReceive('foo')->once()->ordered();
$mock->shouldReceive('bar')->once()->with->(123)->ordered();
$mock->shouldReceive('foo')->once()->ordered();
$mock->shouldReceive('bar')->once()->with->(456)->ordered();

$object->setSomething($mock->proxy());
$object->doSomething();

$mock->verifyMessagesReceived(); // false failure due to the second instance of expectation 'bar' overwriting the first; despite arguments differring.
My only other suggestion is that you really should consider using a proxy object to avoid all chances of name collisions :)

Details (and code) can be seen in the long thread :)
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

Were you going to post code/details somewhere Jenk?

Anyway, I'm at the point in the API (nice to see someone else familiar with this style ;)) where I can test that I think. I just added Specs for ordering as follows:

Code: Select all

public function itShouldObeyOrderingViaSequenceOfOrderedTermCalls()
    {
        $this->_mock->shouldReceive('setName')->with('name')->ordered();
        $this->_mock->shouldReceive('getName')->ordered();
        $this->_mock->setName('name');
        $this->_mock->getName();
        $this->spec($this->_mock->verify())->should->beTrue();
    }

    public function itShouldDisallowMethodCallingIfMethodHasSpecifiedOrder()
    {
        $this->_mock->shouldReceive('getName')->withNoArgs()->ordered();
        $this->_mock->shouldReceive('setName')->once()->withAnyArgs()->ordered();
        try {
            $this->_mock->setName('x');
            $this->fail();
        } catch (PHPMock_Exception $e) {
        }
    }

    public function itShouldAllowMethodCallingOfUnorderedExpectationsInAnyOrder()
    {
        $this->_mock->shouldReceive('getName')->withNoArgs()->ordered();
        $this->_mock->shouldReceive('setName')->once()->withAnyArgs()->ordered();
        $this->_mock->shouldReceive('hasName')->withNoArgs(); // not ordered; call any time
        try {
            $this->_mock->hasName();
            $this->_mock->getName();
            $this->_mock->setName('x');
        } catch (PHPMock_Exception $e) {
            $this->fail();
        }
    }

    public function itShouldReturnSelfInstanceFromOrderedTerm()
    {
        $object = $this->_mock->shouldReceive('getName')->ordered();
        $this->spec($object)->should->beAnInstanceOf('PHPMock_Expectation');
    }
nbenes
Forum Newbie
Posts: 2
Joined: Tue Nov 27, 2007 10:42 am

Post by nbenes »

I'm excited to see a working version of PHPMock, it looks extremely promising...Make sure you support some kind of andReturnReference( $object ) method in there, like SimpleTest does >.>
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Post by Jenk »

The problem lay with the arguments of a call not being validated rigourously, and the ambigous way of cycling through an array. Smalltalks arrays are not ordered, think hash table in Java, and so the mock believed it was receiving the correct message, but with incorrect arguments, and at the incorrect ordering. We swapped for an OrderedCollection, and reinforced the message verification as below.
This was using BlockContexts, which PHP does not support (an anonymous code block, kind of in "suspended animation" if you like) which would be very hard to display in php.

Anyway, the final solution was that we doubly verified the current received call, with it's arguments, is the next expected call. Other scenarios that made it a bit tricky included a mish-mash of ordered/unordered:

Code: Select all

$mock->shouldReceive('someMethod')->twice();
$mock->shouldReceive('someOtherMethod')->with($someArgs)->once()->ordered();
$mock->shouldReceive('foo')->with($moreArgs)->once()->ordered();

// the test..
$mock->proxy()->someMethod();
$mock->proxy()->someOtherMethod($arg1, $arg2);
$mock->proxy()->someMethod();
$mock->proxy()->foo($arg3, $arg4);

$mock->verifyMessagesReceived(); // unexpected failure.
In Smalltalk, each method call is an instance of the class "Message" so we could cycle through expectations easily in a vistor/observer like pattern, and it would verify itself against the message, returning true if the message satisfies the expectation. We had to strengthen this validation technique so that it (in the final chosen order) verified:
  • If the expectation is ordered, then compare if the expectation is at the correct call count to be called.
  • Compared method name
  • Compared the received arg count, vs expected arg count
  • Compared that each argument satisfied it's constraint (exact, alpha, numer, alnum, regex, etc are all supported)
p.s. if you create a message class/object for your framework, it wouldn't be difficult at first glance. It's only a value-object containing method name, and an array of args.
Post Reply