Alright, walk me through TDD one more time...

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

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

Post by Maugrim_The_Reaper »

The main of testing any Setter - what goes in must come out ;)
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

This method doesn't exist...

Code: Select all

$this->expectException(new DNS_Db_Exception('No valid user name has been supplied'));
do I need to write it? (I had to change the Mock::generate to static for it to work :? I thought simpletest was php5, but I guess not)

EDIT: Nevermind, this did the trick:

Code: Select all

try {
            $db = new MC2_Mysql('badhost', 'baduser', 'badpass');
        }
        catch (Exception $e)
        {
            $this->assertEqual(get_class($e), 'MC2_Mysql_Exception');
            $this->assertEqual($e->getMessage(), 'Invalid mysql credentials');
        }
:D
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

expectException() exists in the current released version - 1.0.1beta, and of course in CVS. The new beta was released not long ago (and it needs an update for PHP5.2) so I would check your version. Good adjustment for its absence though ;).

Mock::generate() is static - in older versions of SimpleTest it sometimes breaks in PHP5.1.4+ (another reason to update). If you haven't guessed, you should update SimpleTest for each new PHP release. SimpleTest is moving towards PHP5, but the current track is staying on PHP4 for a while longer.
just on a side note... does somebody want to help explain mock objects? (I've read about them on lastcraft.com, but I could use a little further explaining)
Mocks simulate external classes, interfaces, and resources (see also Stubs). The idea is that each testcase tests a single class in isolation from the rest of the system. For example, if I have a class which generates SQL and executes it using ADOdb, I can't just use ADOdb directly (it would break my tests if it changed). So I mock it. Since the library is not exactly a gleaming example of OOP design (to be honest it's a nightmare to extend), I just mock an outline of it's interfaces.

Code: Select all

class Outline_DatabaseAbstractor {

	public function Execute() {}

	public function SelectLimit() {}

	public function ErrorMsg() {}

	public function Insert_ID() {}

}

class Outline_DatabaseAbstractorResult {

	public $fields = array();

	public function Close() {}

	public function GetArray() {}

}
Once it's Mocked, I then setup the mock with some standard and fixed behaviour. For example, I want the class being tested to use ADOdb correctly. To check this, I need to test that certain ADOdb methods are called, with the correct parameters, and that the return values are correctly handled. This is another reason to Mock - mocks can be told what methods, parameters to expect, and will fail if a test if these expectations are not met. You can't do that with the real library!

Here's a test which check the getByPK() method of a DAO. The DAO generates the SQL, uses ADOdb to execute the SQL, and handles the return results. The outlines above are mocked to simulate ADOdb (it uses separate Connection and Result classes).

Code: Select all

public function testGetByPk() {
    // Mock the results class
        $rs = new MockDBResult();
    // Mock the connection class
        $db = new MockDBAbstract();
    // Tell the mocked Execute method to return a mocked results instance
        $db->setReturnValue('Execute', $rs);
    // Tell the results public $fields property to hold some standard data (tests are fixed)
        $rs->fields = array('item_id'=>1, 'item_name'=>'Item001');
    // Tell the connection class, getByPK() should only call Execute() once with
    // the following SQL statement (using ADOdb auto value quoting here)
        $db->expectOnce('Execute', array('SELECT * FROM test_item WHERE item_id = ?', array(1)));
    // The results class should expect a call to Close()
        $rs->expectOnce('Close');

    // Now perform the actual test

        $dao = new Quantum_Db_Access($db);
    // Item is a Data Object, it proxies CRUD operations to the DAO
    // Item is another of those "outline" classes we use for convenience,
    // since the DAO cannot be used directly (it's proxied to).
        $item = new Item(array(), $dao);
        $item->getByPk(1);

    // Finally check the results in the class being tested
        $this->assertEqual($item->item_id, 1);
        $this->assertEqual($item->item_name, 'Item001');
        $this->assertTrue($item->getExists());
}
Note, the actual implementing code extract....

Code: Select all

public function getByPk($row, $value=null)
    {
        $sql = 'SELECT * FROM ' . $row->getTableName() . ' WHERE ' . $row->getPrimaryKey() .' = ?';
        if(isset($value))
        {
            $result = $this->db->Execute($sql, array( $value ));
        }
        else
        {
            $result = $this->db->Execute($sql, array( $row->getPrimaryKeyValue() ));
        }
        if(!$result)
        {
            throw new Quantum_Db_Exception($sql . '<br /><br />' . get_class($this) . ': ' . $this->db->ErrorMsg());
        }
        if(!empty( $result->fields ))
        {
            $row->import( $result->fields );
            $row->setExists();
        }
        else
        {
            $row->clear(); // no data if no results 
        }
        $result->Close();
    }
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

so how would you "mock" a database connection... say I have some methods like this...

Code: Select all

public function __construct($host, $user, $pass, $name=null)
    {
        $this->connect($host, $user, $pass);
        if(!is_null($name))
        {
            $this->selectDb($name);
        }
    }

    public function connect($host, $user, $pass)
    {
        if($conn = @mysql_connect($host, $user, $pass))
        {
            $this->_conn = $conn;
            return true;
        }
        throw new MC2_Mysql_Exception('Invalid mysql credentials');
    }

    public function isConnected()
    {
        return is_resource($this->_conn);
    }
How could I mock this connection? How could I test something like this without having to set up a real database?
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Code: Select all

class Db
{
    public function connect();
    public function isConnected();
    public function query();
}

class ResultSet
{
    public function get();
    public function next();
    public function end();
}
I'm imagining:

Code: Select all

$db = new Db( ... );
$rs = $db->query( ... );
while (!$rs->end())
{
    echo $rs->get( ... );
    $rs->next();
}
So now think about mocking it :)

Code: Select all

Mock::Generate("Db");
Stub::Generate("ResultSet", "StubResult");

class TestOfWhatever extends UnitTestCase
{
    public function testSomethingHappensWhenNoResultsReturned()
    {
        $stub_rs = new StubResult();
        $stub_rs->setReturnValue("end", true);
        
        $mock_db = new MockDb();
        $mock_db->setReturnValue("isConnected", true);
        $mock_db->setReturnValue("query", $stub_rs);
        
        $whatever = new ClassWhichUsesDb($mock_db);
        $this->assertFalse($whatever->someMethodWhichNeedsResults());
    }
}
It gets more complicated when you need to start returning actual results but you do it by using setReturnValueAt() and knowing the sequence of calls which your object will make on the db (or you probably will do). If it's really not that predictable and various external factors can affect it either you just have to use the real thing, or you do something like I did in swift and write a specialized stub which actually processes your inputs in some loose fashion.
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

OK, thank you, but that's beyond even what I was talking about (but helpful none-the-less for my next step). All I meant is how do I test the Db Class is connecting to a database (I know you can't w/out a database, so I wanted to know how you would mock one)...

Code: Select all

class Db
{
    public function connect();
    public function isConnected();
    public function query();
}

class ResultSet
{
    public function get();
    public function next();
    public function end();
}

class TestDb extends UnitTestCase
{
    public function testConnectWithGoodParams()
    {
        $db = new Db;
        $db->connect($these, $are, $good, $params);
        $this->assertTrue($db->isConnected());
    }
}
Is there even any point to testing that? It's either true or an exception is thrown... not a whole lot to test... :?
User avatar
Chris Corbyn
Breakbeat Nuttzer
Posts: 13098
Joined: Wed Mar 24, 2004 7:57 am
Location: Melbourne, Australia

Post by Chris Corbyn »

Don't test that the Db can connect to a Db. That has nothing to with your design, it's just a detail to do with the server/configuration. Just assume the Db does connect. You could factor connect() into a method which returns a boolean and then simply get your mock to return boolean false if you wanted to simulate a connection failing. The implementation of how it returns false is up to you, although if (@mysql_connect( ... )) seems pretty solid :)
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

that's the answer I needed... and now I have the answer for the next step as well :wink: thanks!
User avatar
Weirdan
Moderator
Posts: 5978
Joined: Mon Nov 03, 2003 6:13 pm
Location: Odessa, Ukraine

Post by Weirdan »

All I meant is how do I test the Db Class is connecting to a database
Well, that's the main problem in TDD: you can effectively test the interactions with entities external to host language runtime only by side-effects those interactions produce. E.g. if your db server logged connection attempts you would read&parse the log file to check if your code attempted any connects. That's the reason for you to keep your borderline classes (those that interact with 'outside word') as simple as possible. Once you created the abstraction layer you can explore your application design further, mocking external entities (actually classes which provide access to those external entities) where necessary.
Post Reply