Simpletest's mock objects seem inflexible

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
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Simpletest's mock objects seem inflexible

Post by Ambush Commander »

I've been working with them and already I'm having problems emulating what the actual function is supposed to do. For instance, take this call:

Code: Select all

$array = array();
            while($res->fetchInto($row, DB_FETCHMODE_ASSOC)) {
                $array[$row['id']] = $row;
            }
The function sets $row as something because it was passed by reference. How the heck do you do that in SimpleTest (yes, I read the docs. Do I have to extend the class?)
User avatar
nielsene
DevNet Resident
Posts: 1834
Joined: Fri Aug 16, 2002 8:57 am
Location: Watertown, MA

Post by nielsene »

While we wait for the expert (McGruff) :)

Let me make sure I understand what you're doing.

You have a ResultSet fo come kind. When you want to get the next row, you have it return a reference to the row. (Probably because you wanted a pure boolen true/false return from the function?)

You are now trying to mock the ResultSet and are having difficulties getting it to give you a reference back.

I thought maybe the SimpleStub base class would help out, but its setReturnReference appears to only be useful for returning a reference as the return value, not as an argument. So I think you're right and that you'll need to do something a little different.

I'ld try emailing the simpletest email list. Marcus is great about suggesting simple workarounds....
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

Can you post the test method?
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

I have to warn you, I've changed the method since I last posted my question, particularly I changed the function to one with a more conventional method of returning results.

Code: Select all

$this->obj->db =& new MockDB_mysql($this);
        $result_email =& new MockDB_result_email($this);
        $result_ftp =& new MockDB_result_ftp($this);
        
        $result_email->setReturnValue('fetchRow', false);
        $result_email->setReturnValueAt(0, 'fetchRow',
          array(
            'id' => 4
            ,'address' => 'arcezy@gmail.com'
            ,'last_md5' => '072734bca324930cf0c2e48710425814'
          ));
        
        $result_ftp->setReturnValue('fetchRow', false);
        $result_ftp->setReturnValueAt(0, 'fetchRow',
          array(
            'id' => 2
            ,'server' => 'www.thewritingpot.com'
            ,'username' => 'backup'
            ,'password' => 'pass'
            ,'max_copies' => 'max_copies'
            ,'last_md5' => '072734bca324930cf0c2e48710425814'
          ));
        
        $this->obj->db->setReturnValue('query', &$result_email, array(' SELECT `id`, `interval`, `last_md5`  FROM `backup_email`  WHERE ((NOW() - `interval`) >
      `last_update`) '));
        $this->obj->db->setReturnValue('query', &$result_ftp, array(' SELECT `id`, `interval`, `last_md5`  FROM `backup_ftp`  WHERE ((NOW() - `interval`) >
      `last_update`) '));
        
        $this->obj->getOperations();
        
        //var_dump($this->obj->list_backups);
        
        $this->assertEqual($this->obj->list_backups['backup_email'][4]['id'],4);
        $this->assertEqual($this->obj->list_backups['backup_email'][4]['address'],'arcezy@gmail.com');
Unfortunantely, I'm not very happy with this test case either, because the set return values are inflexible for varying SQL queries. Since the query is fairly constant with only a few variations, I'm considering moving it to its own function, but then I'd have problems mocking it.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

Setting object properties without named setters is cheating a bit. An object should have a well-defined interface.

For example you could pass the db object in to the primary class (ie the one being tested). Or, if you don't want the db object exposed in the design like that, you can use a factory method and partially mock in the test (the procedure is explained in the SimpleTest docs - but do ask if you'd like me to explain).

Either way, rather than setting expectations for the mock db objectlike this:

Code: Select all

$this->obj->db->setReturnValue(...)
..just do this:

Code: Select all

$db =& new MockDB_mysql($this);
$db->setReturnValue(...)
$db->setReturnValue(...)
..then pass it in or set as a return ref for the partially mocked method.

You probably ought to set some expectOnce/expectCallCount expectations as well (don't forget to tally up: $mock_object->tally() ).

Assertions like this;

Code: Select all

$this->assertEqual($this->obj->list_backups['backup_email'][4]['id'],4);
..aren't good because you're peeking under the bonnet. Is there another way to test if the getOperations call did what it was supposed to do? if all it's doing is teeing up the class I'd be tempted to do that in the constructor - maybe you can test another method of the primary class which actually does something? (I might not have grasped what you're doing exactly).
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

McGruff wrote:Setting object properties without named setters is cheating a bit. An object should have a well-defined interface.

For example you could pass the db object in to the primary class (ie the one being tested). Or, if you don't want the db object exposed in the design like that, you can use a factory method and partially mock in the test (the procedure is explained in the SimpleTest docs - but do ask if you'd like me to explain).
Ah... I think I understand. So it's a good idea not to use $this-> variables as things you stick variables in for easy access from functions? Edit No, I was mistaken, it's not a good idea to directly set object properties, there should always be a method above it.
McGruff wrote:Either way, rather than setting expectations for the mock db objectlike this:

Code: Select all

$this->obj->db->setReturnValue(...)
..just do this:

Code: Select all

$db =& new MockDB_mysql($this);
$db->setReturnValue(...)
$db->setReturnValue(...)
..then pass it in or set as a return ref for the partially mocked method.
That makes sense. I suppose it's cleaner.
You probably ought to set some expectOnce/expectCallCount expectations as well (don't forget to tally up: $mock_object->tally() ).
Hmm... I'll incorporate those too.
Assertions like this;

Code: Select all

$this->assertEqual($this->obj->list_backups['backup_email'][4]['id'],4);
..aren't good because you're peeking under the bonnet. Is there another way to test if the getOperations call did what it was supposed to do? if all it's doing is teeing up the class I'd be tempted to do that in the constructor - maybe you can test another method of the primary class which actually does something? (I might not have grasped what you're doing exactly).
Hmm... I think I need to post more code. Originally, getOperations cycled through an array of table names and performed a specific query on each of them. I suppose, then, that getOperations should be simply the cycling through the array, and then an alternate function/class handles the sql.

Edit: I just reread the section on Partial Mocks, and it's really interesting. However, I'm a bit worried about the last section:
We could even go as far as to mock every method except one we actually want to test.

This last situation is all rather hypothetical, as I haven't tried it. I am open to the possibility, but a little worried that forcing object granularity may be better for the code quality. I personally use partial mocks as a way of overriding creation or for occasional testing of the TemplateMethod pattern.

It's all going to come down to the coding standards of your project to decide which mechanism you use.
I'm not exactly sure what mechanism I should use (this is a pretty small project that's meant to be deployed only on one machine but maybe be deployed on a wider scale and I want to be flexible but I also want to get it finished.) The reason why I turned to unit testing was that I was throwing variables around until the properties table was a huge mess and I couldn't figure out where I stored any particular bit of information. I'm not even sure if I should be unit testing this sort of thing...
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

Do you mean you're not sure whether to pass the $db object in or to instantiate in a factory method?
User avatar
Ambush Commander
DevNet Master
Posts: 3698
Joined: Mon Oct 25, 2004 9:29 pm
Location: New Jersey, US

Post by Ambush Commander »

Yeah, basically.
lastcraft
Forum Commoner
Posts: 80
Joined: Sat Jul 12, 2003 10:31 pm
Location: London

Post by lastcraft »

Hi...

This forum does seem to have taken off since I last looked :).

I am afraid I don't have the time to monitor another forum, but if there are queries about the innards of SimpleTest here, feel free to mail the thread link to the SimpleTest mail list or to me and I'll try to reply here.

Regarding emulating result sets, I tend to do this...

Code: Select all

$iterator = &new MockResultSet();
$iterator->setReturnValueAt(0, 'next', array(...));
$iterator->setReturnValueAt(1, 'next', array(...));

$connection = &new MockConnection();
$connection->setReturnReference('query', $iterator);
If that doesn't work then I will extend the mock class or even hand code a mock for the task. I guess as that's the way I started I am more willing than most to hand code when SimpleTest doesn't do what I want.

yours, Marcus

p.s. No I didn't miss off the $this ;). It's unnecessary in the current CVS code. The tally() call is deprecated too as it's now automatic.
McGruff
DevNet Master
Posts: 2893
Joined: Thu Jan 30, 2003 8:26 pm
Location: Glasgow, Scotland

Post by McGruff »

OK in general, I'd say you wouldn't want to expose an object in the design any more than you have to. If object foo is the only one which uses object bar, instantiate it in a factory method - then partially mock the factory method in foo so you can supply it with a mock bar in tests.

On the other hand, if other objects in the script are also using the bar object, you'll be passing it in either in the constructor or one of the foo methods. Pass in a mock in the tests.

With database objects, it's quite likely that they will be used in a variety of places in the script. Sometimes a Registry is used to save passing around frequently used objects like this all over the place. In tests, set the Registry up to return a mock object.

Strictly speaking, if you are test-driven-designing, you wouldn't add a Registry into the mix until a need for one emerges (XP emphasises doing just what you need for the current requirement and no more). You should be prepared to read between the lines a little bit though if you are sure you will need one later.

So, take your pick... :)
Post Reply