More complete ProjectTestCase

In creating a new project, we need to check the following:

So to perform this task, we will modified the test code. function testProjectDaoCanCreateNewProject() { $project = new Project(); $project->ID = 1; $project->Name = "Project 1"; $project->CreatorUserName = "Customer A"; $project->ManagerUserName = "Manager A"; $customer = new TimeTrackerUser(); $customer->ID = 1; $customer->Name = "Customer A"; $manager = new TimeTrackerUser(); $manager->ID = 2; $manager->Name = "Manager A"; if(($conn = $this->connection) instanceof MockTSqlMapper) { //return the customer and manager $conn->setReturnValue('queryForObject', $customer, array('GetUserByName', 'Customer A')); $conn->setReturnValue('queryForObject', $manager, array('GetUserByName', 'Manager A')); //project does not exist $conn->setReturnValue('queryForObject', null, array('GetProjectByName', 'Project 1')); $param['project'] = $project; $param['creator'] = $customer->ID; $param['manager'] = $manager->ID; $conn->setReturnValue('insert', true, array('CreateNewProject', $param)); $conn->setReturnReference('queryForObject', $project, array('GetProjectByID', 1)); //we expect queryForObject to be called 3 times $conn->expectMinimumCallCount('queryForObject', 3); $conn->expectAtLeastOnce('insert'); } $this->assertTrue($this->dao->createNewProject($project)); $this->assertEqual($this->dao->getProjectByID(1), $project); }

It may seem very weird that there is so much code in the tests and why we even bother to write all these test codes. Well, using the above test code we have the following advantages.

Advantages of Mock
  1. we don't need a real database base connection to test the code, this means we can start relying on tested code ealier
  2. when a test fails we know that problem is not part of the database
  3. when a test fail, we can quickly pin point the problem
  4. the test suite gives us the confidence to refactor our code

Of couse, the test will not be able to cover the higher interactions, such as the user interface, so intergration or functional web test will be used later on.

So how did we come up with the above tests? We started simple, then we ask what sort of things it should handle. We assume that the connection object work as expect or known to be unexpected and see how the method we want to test handle these situations.

If we run the above test, we will be faced with numerous errors. First will be that the TimeTrackerUser can not be found.

Creating a new User Class

Notice that the Project class contains CreatorUserName and ManagerUserName properties. So at some point we are going to need at least one User class. We shall name the class as TimeTrackerUser and save it as APP_CODE/TimeTrackerUser.php <?php Prado::using('System.Security.TUser'); Prado::using('System.Security.TUserManager'); class TimeTrackerUser extends TUser { private $_ID; public function __construct() { parent::__construct(new TUserManager()); } public function getID(){ return $this->_ID; } public function setID($value) { if(is_null($this->_ID)) $this->_ID = $value; else throw new TimeTrackerUserException( 'timetracker_user_readonly_id'); } } ?>

Custom Exceptions

We enforce that the ID of the user to be read-only once it has been set by throwing a custom exception. Prado's exception classes uses a string key to find a localized exception string containing more detailed description of the exception. The default exception messages are stored in the framework/Exceptions/messages.txt. This file location can be changed by overriding the getErrorMessageFile() method of TException class. We define a custom exception class for all Time Tracker application exceptions as TimeTrackerException and save the class as APP_CODE/TimeTrackerException.php.

<?php class TimeTrackerException extends TException { /** * @return string path to the error message file */ protected function getErrorMessageFile() { return dirname(__FILE__).'/exceptions.txt'; } } ?>

We then create a exceptions.txt file in the APP_CODE directory with the following content.

timetracker_user_readonly_id = Time tracker user ID is read-only.

Additional parameters passed in the exception constructor can be added the message string using {0} as the first additional parameter, and {1} as the second additional parameter, and so on. For example, suppose we want to raise the follow exception.

throw new TimeTrackerException('project_exists', $projectName);

The exception error message in exceptions.txt may contain something like:

project_exists = Time tracker project '{0}' already exists.

Completing the test case

From the unit test code, we can pretty much see what the implementation for createNewProject() will look like.

public function createNewProject($project) { $sqlmap = $this->getConnection(); $creator = $sqlmap->queryForObject('GetUserByName', $project->CreatorUserName); $manager = $sqlmap->queryForObject('GetUserByName', $project->ManagerUserName); $exists = $sqlmap->queryForObject('GetProjectByName', $project->Name); if($exists) { throw new TimeTrackerException( 'project_exists', $project->Name); } else if(!$creator || !$manager) { throw new TimeTrackerException( 'invalid_creator_and_manager', $project->Name, $project->CreatorUserName, $project->ManagerUserName); } else { $param['project'] = $project; $param['creator'] = $creator->ID; $param['manager'] = $manager->ID; return $sqlmap->insert('CreateNewProject', $param); } }
Tip: A hierachy of exception class can be used to have fine exception handling. Since this is a small project and for simplicity, we shall use the application level TimeTrackerException exception class for most exception cases.