TDD is a way to explore some ideas. There is no "right" way to do this so feel free to suggest something different if you like.
My first idea is that user input will come from a Request object (nothing revolutionary you see these a lot). For now, all we need to know about request is that the form values are available via a bunch of named getters: getFoo(), getBar(), etc. If we've got as far as the NewGig class, we know that all required form fields have been submitted and are valid values.
The Request object will be mocked in the test. In the test case, first write a skeleton class to mock:
Code: Select all
class NewGigRequest
{
function getFoo()
{
}
function getBar()
{
}
}
You need to generate the mock object:
NewGig will need to interact with a Database object - another mock. We could substitute a different db interface later if there's one you're used to, but for now I'll use this:
Code: Select all
class Database
{
function execute()
{
}
}
// generate the mock object
mock::generate('Database');
In the test method, the first thing to do is define the expected behaviour of the mock objects.
Code: Select all
class TestOfNewGig extends UnitTestCase
{
// etc
function test()
{
$db =& new MockDatabase($this);
$db->expectCallCount('execute', 2);
$db->setReturnValue('execute', 1);
}
}
The mock is instantiated, "expectCallCount" states that we expect the "execute" method to be called twice, and "setReturnValue" states that it should always return a value of one.
If we do something similar for NewGigRequest:
Code: Select all
class TestOfNewGig extends UnitTestCase
{
// etc
function test()
{
$db =& new MockDatabase($this);
$db->expectCallCount('execute', 2);
$db->setReturnValue('execute', 1);
$request =& new MockNewGigRequest($this);
$request->expectCallCount('getFoo', 1);
$request->expectCallCount('getBar', 1);
}
}
These mock objects will have to be introduced to the primary class (NewGig). I'll come back to that in a moment. First, let's pause to look at what we've done so far. The main point is that Database and NewGigRequest don't have to exist in order to test NewGig. The skeleton classes simply define the interfaces we'd like to have, and the expectations allow us to control how these dummy objects should behave in the test (obviously you'll need to edit this somewhat: I'm sure you've got more in the form submission than just "foo" and "bar"). This allows us to write a test for NewGig which will describe how it interacts with neighbouring objects in the design which have yet to be implemented. We're free to play around with some design ideas and change them at any time.
Once the NewGig test is passing, the mock expectations & interfaces provide a blueprint for the next stage in the design where you'd write implementations for the mocks which provide the kind of behaviour defined in the NewGig test case. However, in the NewGig test case, we're not getting bogged down in the details of how these should work - we'll figure that out later.
We might not have found anything to mock in NewGig. The crucial point is to always think about responsibilities. The choice of request & database objects was made because these felt like separate responsibilities and thus should be encapsulated in new objects. The more experience you have with OOP the better you get at making these decisions (I hope to figure it out myself some day). The general point is that, whenever you discover a responsibility that doesn't fit well in the class under test, you've just discovered a new class in the design. Mock it, deferring implementation until later when you've got the test of the primary class running green.
I'm going to assume that you've read through
http://www.lastcraft.com/partial_mocks_ ... tation.php. If I've made too big a jump please let me know and I'll explain in more detail.
The NewGigRequest object is going to be passed in to NewGig in the constructor. That's easy. However, the Database object will be instantiated by a factory method in NewGig:
Code: Select all
function &_Database()
{
require_once('path/to/MysqlDatabase.php');
return new MysqlDatabase(DB_HOST, DB_USER, DB_PASS);
}
Mock objects don't exist as real classes: partial mocks on the other hand are real classes with a few methods knocked out for testing. As explained in the SimpleTest docs, this lets you inject a mock into the test:
Code: Select all
mock::generatePartial('NewGig',
'PartialNewGig',
array('_Database'));
class TestOfNewGig extends UnitTestCase
{
// etc
function test()
{
//etc
$new_gig =& new PartialNewGig($this);
$new_gig->setReturnReference('_Database', $db);
}
}
All the other methods will work exactly as normal.
I mentioned before that NewGig would update the db and create a view. If we assume the latter is contained in a "_print" method, we can knock that out in the partial mock of NewGig (attempting to generate a browser page would upset the test). Finally a full test:
Code: Select all
mock::generatePartial('NewGig',
'PartialNewGig',
array('_Database', '_print'));
class TestOfNewGig extends UnitTestCase
{
// etc
function test()
{
$db =& new MockDatabase($this);
$db->expectCallCount('execute', 2);
$db->setReturnValue('execute', 1);
$request =& new MockNewGigRequest($this);
$request->expectCallCount('getFoo', 1);
$request->expectCallCount('getBar', 1);
$new_gig =& new PartialNewGig($this);
$new_gig->setReturnReference('_Database', $db);
$new_gig->expectOnce('_print');
$new_gig->NewGig($request);
$this->assertIdentical($new_gig->try(), true);
$new_gig->tally();
$db->tally();
$request->tally();
}
}
Can you write some code which passes this test? I'm afraid you'll also need to edit the test case a little: there will be more form values tha just foo and bar, and probably more than just two queries to make. You could also substitute your own database object - and set appropriate expectations.
I hope I haven't rushed through this too quickly. I'm struggling a bit to find enough time to do this justice but I will promise not to give up until you are fully test-infected. Any questions, let fly.