SimpleTest Mock - getArgumentsAt() ?

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
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

SimpleTest Mock - getArgumentsAt() ?

Post by Chris Corbyn »

I want to collect the arguments in a Mock object myself. I can make an identical expectation on the arguments passed by using expectAt() but this is too rigid here as I simply want to analyze a component in a passed object, not the entire object.

Does anybody know of a reasonable way to do something like this?

Code: Select all

Mock::Generate("SomeClass", "MockSomeClass");

class TestOfSomeClass extends UnitTestCase 
{
    public function testMessageHasTheToHeaderRemovedWhenPassed()
    {
        $blah = new Whatever();
        $mock = new MockSomeClass();
        $mock->expectOnce("someMethod");
        $blah->setListener($mock);
        $blah->doSomething();
        //Here's the tricky bit
        $passed_args = $mock->getArgumentsAt(0, "someMethod");
        $this->assertFalse($passed_args[1]->hasTo());
    }
}
I'm tempted to use a partial mock and copy the argument into a property before passing it to someMethod() but that feels awfully unclean.
lastcraft
Forum Commoner
Posts: 80
Joined: Sat Jul 12, 2003 10:31 pm
Location: London

Post by lastcraft »

Hi.

This is a repost of the Sitepoint thread, but basically...

1) Extend the mock:

Code: Select all

Mock::generate('MyClass', '_MockMyClass');
class MockMyClass extends _MockMyClass {
    function someMethod($object, ...) {
        $test = SimpleTest::getContext()->getTest();
        $test->assertFalse($object->hasTo());
        return parent::someMethod($object, ...);
    }
}
2) Create a custom expectation:

Code: Select all

class NotHasTo extends SimpleExpectation {
    function test($object) {
        return ! $object->hasTo();
    }
}
The in the test...

Code: Select all

$mock->someMethod(new NotHasTo(), ...);
yours, Marcus
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Ha, yeah. Didn't wanna come back with [Solved] and a link to SitePoint being a moderator here and all that :P

Cheers dude.
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post by John Cartwright »

d11wtq wrote:Ha, yeah. Didn't wanna come back with [Solved] and a link to SitePoint being a moderator here and all that :P

Cheers dude.
Nothing wrong with that, all is fair in love and war.. and programming. :wink:
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

This seems incredibly cool and I need to do such a thing right now.
What's the deal with the hasTo() stuff? Is that specific to this example?
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

ole wrote:This seems incredibly cool and I need to do such a thing right now.
What's the deal with the hasTo() stuff? Is that specific to this example?
Yeah it was a method to check for a To: header in an email.

I just wrote custom mock objects to get around it, overriding the method I wanted to catch parameters from, with a version which makes assertions on the arguments.

Code: Select all

class CustomMailSendExpectation extends Swift_Plugin_MailSend
{
	protected $testcase = null;
	
	public function __construct($test)
	{
		parent::__construct();
		$this->testcase = $test;
	}
}

//Some custom expectations - ouch, yes, yes I know, perhaps I've dug myself into a hole here!
class MailSendExpectingNoTo extends CustomMailSendExpectation
{
	public function doMail($to, $subject, $message, $headers)
	{
		if ($headers->has("To")) $this->testcase->fail("The headers should NOT have a To field in them");
		else $this->testcase->pass();
	}
}


// snip //

public function testHeadersDoNotContainThe_To_Field()
	{
		$swift = new Swift(new Swift_Connection_NativeMail());
		$mailsend = new MailSendExpectingNoTo($this);
		
		$swift->attachPlugin($mailsend, "_MAIL_SEND"); //Override the MailSend plugin with a custom mock
		
		$recipients = new Swift_Address("test@bar.tld", "Test");
		$sender = "foobar@bar.com";
		$subject = "Foo Bar";
		$body = "Foo test\r\nBar";
		$message = new Swift_Message($subject, $body);
		
		$swift->send($message, $recipients, $sender);
	}
Marcus provided some more re-usable solutions though by extending the unit testing framework :) The way expectations are handled is pretty neat.
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

I ended up doing this:

Code: Select all

<?php
require_once 'Osis/Form/Widget.php';
require_once 'Osis/Form/Validation/Interface.php';
require_once 'Osis/Form/Container/Master/Interface.php';
require_once 'Osis/Form/Renderer/IsA/Interface.php';
Mock::generate('Osis_Form_Validation_Interface','_Mock_Osis_Form_Validation');
Mock::generate('Osis_Form_Container_Master_Interface', 'Mock_Osis_Form_Container_Master_Interface');
Mock::generate('Osis_Form_Retriever_Interface', '_Mock_Osis_Form_Retriever');
Mock::generate('Osis_Form_Renderer_IsA_Interface', '_Mock_Osis_Form_Renderer');
class Mock_Osis_Form_Validation extends _Mock_Osis_Form_Validation
{
    private $_expectedDispatch;
    public function setExpectedDispatch($expectedDispatch)
    {
        $this->_expectedDispatch = $expectedDispatch;
    }
    public function dispatch($which)
    {
        $test = SimpleTest::getContext()->getTest();
        $test->assertIdentical($which, $this->_expectedDispatch);
        parent::dispatch($which);
    }
}
class Mock_Osis_Form_Retriever extends _Mock_Osis_Form_Retriever
{
    private $_expectedMaster;
    public function setExpectedMaster($expectedMaster)
    {
        $this->_expectedMaster = $expectedMaster;
    }
    public function initialize(Osis_Form_Container_Master_Interface $master)
    {
        $test = SimpleTest::getContext()->getTest();
        $test->assertIdentical($master, $this->_expectedMaster);
        parent::initialize($master);
    }
}
class Mock_Osis_Form_Renderer extends _Mock_Osis_Form_Renderer
{
    private $_expectedInput;
    public function setExpectedInput($expectedInput)
    {
        $this->_expectedInput = $expectedInput;
    }
    public function setDataFromEntity($data)
    {
        $test = SimpleTest::getContext()->getTest();
        $test->assertIdentical($data->input, $this->_expectedInput);
        parent::setDataFromEntity($data);
    }
}

class Osis_Form_WidgetTest extends Osis_UnitTest
{
    /**
     * @var Osis_Form_Widget
     */
    public $inst;
    public function setUp()
    {
        $this->inst = new Osis_Form_Widget(
            new Osis_Form_Id('foo'),
            $this->mockValidation = new Mock_Osis_Form_Validation()
        );
        $this->inst->setRetriever(
            $this->mockRetriever = new Mock_Osis_Form_Retriever()
        );
        $this->inst->setRenderer(
            $this->mockRenderer = new Mock_Osis_Form_Renderer()
        );
    }
    public function tearDown()
    {
        $this->mockValidation->tally();
        $this->mockRetriever->tally();
        $this->mockRenderer->tally();
    }
    public function testAll()
    {
        $this->mockRenderer->setReturnValue('render', $renderReturn = uniqid());
        $this->mockRenderer->expectOnce('render');
        $this->mockRenderer->setExpectedInput($inputReturn = uniqid());

        $mockMaster = new Mock_Osis_Form_Container_Master_Interface();
        $this->mockRetriever->setExpectedMaster($mockMaster);
        $this->mockRetriever->expectOnce('initialize');
        $this->mockRetriever->setReturnValue('getInput', $inputReturn);
        $this->mockRetriever->expectCallCount('getInput', 2);

        $this->mockValidation->setExpectedDispatch(Osis_Form_Widget::EVENT_INIT);
        $this->mockValidation->expectCallCount('dispatch', 2);

        $this->inst->initialize($mockMaster);

        $this->mockValidation->setExpectedDispatch(Osis_Form_Widget::EVENT_RENDER);
        $this->assertEqual($renderReturn, $this->inst->render());
        $this->assertEqual($inputReturn, $this->inst->getInput());
    }
}
That's testing an object that deals with a number of other objects (a retriever, renderer, master and validation object). It's lovely when it call comes together though. I must admit this was considerably harder to test than write.

An actual getArgumentsAt() method would be really nice in my opinion.
lastcraft
Forum Commoner
Posts: 80
Joined: Sat Jul 12, 2003 10:31 pm
Location: London

Post by lastcraft »

Hi...

The example is quite complicated and I don't entirely understand it. Why could you not just place the expected object in the expectOnce() call? Is it because you want this to tie up with something another object passes in? I've kind of lost the plot on this one :(.

yours, Marcus
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

Why could you not just place the expected object in the expectOnce() call?
Well Marcus, you just removed a whole lot of lines from my test. To answer your question: Because I hadn't RTFMed!
Is it because you want this to tie up with something another object passes in?
No its because I'm an idiot.
I've kind of lost the plot on this one Sad.
No I have, you're doing just fine. :)

I have a bit of a thing with mock objects....I read "The mock object pattern" in Jason Sweat's patterns book and he said they help you out when you are experiencing a "chicken and egg scenario" and you can use them to stand in for objects that do not yet exist and that really confused me because obviously Mock::generate() needs that all important first parameter. Annnyyway I think I'm finally getting to grips with them. As I understand it mocks are
  • To test an object's interactions with another; without mocks this is difficult
  • To simplify complex or expensive emulations with predefined returns such as standing in for a databse
  • To make tests less fragile (I only realised today you can mock interfaces which is very nice)
Anyway here's my new code, see much better ahhhh:

Code: Select all

<?php
require_once 'Osis/Form/Widget.php';
require_once 'Osis/Form/Validation/Interface.php';
require_once 'Osis/Form/Container/Master/Interface.php';
require_once 'Osis/Form/Renderer/IsA/Interface.php';
Mock::generate('Osis_Form_Validation_Interface','Mock_Osis_Form_Validation');
Mock::generate('Osis_Form_Container_Master_Interface', 'Mock_Osis_Form_Container_Master_Interface');
Mock::generate('Osis_Form_Retriever_Interface', 'Mock_Osis_Form_Retriever');
Mock::generate('Osis_Form_Renderer_IsA_Interface', 'Mock_Osis_Form_Renderer');

class Osis_Form_WidgetTest extends Osis_UnitTest
{
    /**
     * @var Osis_Form_Widget
     */
    public $inst;
    public function setUp()
    {
        $this->inst = new Osis_Form_Widget(
            new Osis_Form_Id('foo'),
            $this->mockValidation = new Mock_Osis_Form_Validation()
        );
        $this->inst->setRetriever(
            $this->mockRetriever = new Mock_Osis_Form_Retriever()
        );
        $this->inst->setRenderer(
            $this->mockRenderer = new Mock_Osis_Form_Renderer()
        );
    }
    public function tearDown()
    {
        $this->mockValidation->tally();
        $this->mockRetriever->tally();
        $this->mockRenderer->tally();
    }
    public function testAll()
    {
        $this->mockRetriever->expectOnce('initialize', array($mockMaster));
        $this->mockRetriever->setReturnValue('getInput', $inputReturn = uniqid());
        $this->mockRetriever->expectCallCount('getInput', 2);

        $this->mockValidation->expectCallCount('dispatch', 2);
        $this->mockValidation->expectAt(0, 'dispatch', array(Osis_Form_Widget::EVENT_INIT, $this->inst));
        $this->mockValidation->expectAt(1, 'dispatch', array(Osis_Form_Widget::EVENT_RENDER, $this->inst));

        $mockMaster = new Mock_Osis_Form_Container_Master_Interface();
        $this->inst->initialize($mockMaster);

        $this->mockRenderer->setReturnValue('render', $renderReturn = uniqid());
        $this->mockRenderer->expectOnce('render');
        $this->assertEqual($renderReturn, $this->inst->render());

        $this->assertEqual($inputReturn, $this->inst->getInput());
    }
}
sike
Forum Commoner
Posts: 84
Joined: Wed Aug 02, 2006 8:33 am

Post by sike »

hey ole,

your test is hard to understand (at least for me) so i suggest splitting the testAll in more meaningful tests methods (eg. testWidgetReadsDataFromRetriever).

cheers
Chris
User avatar
Ollie Saunders
DevNet Master
Posts: 3179
Joined: Tue May 24, 2005 6:01 pm
Location: UK

Post by Ollie Saunders »

so i suggest splitting the testAll in more meaningful tests methods (eg. testWidgetReadsDataFromRetriever).
I thought that was be really difficult to do but I guess I should give it a go. It is difficult to understand. It was quite difficult to write actually.
User avatar
feyd
Neighborhood Spidermoddy
Posts: 31559
Joined: Mon Mar 29, 2004 3:24 pm
Location: Bothell, Washington, USA

Post by feyd »

ole wrote:I thought that was be really difficult to do but I guess I should give it a go. It is difficult to understand. It was quite difficult to write actually.
That's often a "bad" smell. :)
Post Reply