More complete ProjectTestCase
In creating a new project, we need to check the following:
- that the project does not already exists, i.e. no duplicate project name
- that the project creator and project manager exists when a new project is created
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
- we don't need a real database base connection to test the code,
this means we can start relying on tested code ealier
- when a test fails we know that problem is not part of the database
- when a test fail, we can quickly pin point the problem
- 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.