diff options
40 files changed, 1589 insertions, 0 deletions
| diff --git a/.gitattributes b/.gitattributes index 6ec23882..565f4fa6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1066,6 +1066,44 @@ demos/sqlmap-sample/tests/PersonTest.php -text  demos/sqlmap-sample/tests/readme.txt -text  demos/sqlmap-sample/tests/run_tests.php -text  demos/sqlmap-sample/tests/sqlmap.xml -text +demos/time-tracker/index.php -text +demos/time-tracker/protected/.htaccess -text +demos/time-tracker/protected/APP_CODE/BaseDao.php -text +demos/time-tracker/protected/APP_CODE/Project.php -text +demos/time-tracker/protected/APP_CODE/ProjectDao.php -text +demos/time-tracker/protected/APP_CODE/TimeTrackerException.php -text +demos/time-tracker/protected/APP_CODE/TimeTrackerUser.php -text +demos/time-tracker/protected/APP_CODE/exceptions.txt -text +demos/time-tracker/protected/controls/Layout.php -text +demos/time-tracker/protected/controls/Layout.tpl -text +demos/time-tracker/protected/controls/TopicList.php -text +demos/time-tracker/protected/controls/TopicList.tpl -text +demos/time-tracker/protected/data/time_tracker.db -text +demos/time-tracker/protected/pages/Docs/CreateBusinessCode.page -text +demos/time-tracker/protected/pages/Docs/GettingStarted.page -text +demos/time-tracker/protected/pages/Docs/Home.page -text +demos/time-tracker/protected/pages/Docs/Introduction.page -text +demos/time-tracker/protected/pages/Docs/MoreTests.page -text +demos/time-tracker/protected/pages/Docs/UserClassAndExceptions.page -text +demos/time-tracker/protected/pages/Docs/UsingSQLMap.page -text +demos/time-tracker/protected/pages/Docs/WritingFunctionalTest.page -text +demos/time-tracker/protected/pages/Docs/WritingUnitTest.page -text +demos/time-tracker/protected/pages/Docs/config.xml -text +demos/time-tracker/protected/pages/Docs/db.png -text +demos/time-tracker/protected/pages/Docs/functional_test1.png -text +demos/time-tracker/protected/pages/Docs/functional_test2.png -text +demos/time-tracker/protected/pages/Docs/preface.page -text +demos/time-tracker/protected/pages/Docs/project1.png -text +demos/time-tracker/protected/pages/Docs/unit_test1.png -text +demos/time-tracker/protected/pages/Docs/unit_test2.png -text +demos/time-tracker/protected/pages/Docs/unit_test3.png -text +demos/time-tracker/tests/functional.php -text +demos/time-tracker/tests/functional/HelloPradoTestCase.php -text +demos/time-tracker/tests/unit.php -text +demos/time-tracker/tests/unit/AddUserToProjectTestCase.php -text +demos/time-tracker/tests/unit/CreateNewProjectTestCase.php -text +demos/time-tracker/tests/unit/ProjectDaoTestCase.php -text +demos/time-tracker/tests/unit/ProjectTestCase.php -text  docs/application.xml -text  docs/specs/application.dtd -text  docs/specs/application.xsd -text diff --git a/demos/quickstart/themes/PradoSoft/style.css b/demos/quickstart/themes/PradoSoft/style.css index 782697ae..d7e94032 100644 --- a/demos/quickstart/themes/PradoSoft/style.css +++ b/demos/quickstart/themes/PradoSoft/style.css @@ -517,4 +517,62 @@ pre code  	background-color:#ffffee;
  	font-family: "Courier New", Courier, mono;
  	margin: 0.2em;
 +}
 +
 +div.tip, div.info, div.note
 +{
 +	border:1px solid #0cf;
 +	padding:1em;
 +	margin: 1em 2em;
 +	background-color: #eff;
 +}
 +
 +div.info
 +{
 +	border-color: #32CD32;
 +	background-color: #EBFFCE;
 +}
 +
 +div.note
 +{
 +	border-color: Orange;
 +	background-color: #FFF5E1;
 +}
 +
 +div b.tip
 +{
 +	font-size: 1em;
 +	padding-right: 0.5em;
 +}
 +
 +img.figure
 +{
 +	display: block;
 +	margin: 1em auto;
 +	background-color: White;
 +	padding: 15px;
 +	border: 1px solid #eee;
 +}
 +
 +div.caption
 +{
 +	text-align: center;
 +}
 +
 +table.tabular, table.tabular td, table.tabular th
 +{
 +	border: 1px solid #ccc;
 +	border-collapse: collapse;
 +	padding: 0.3em;
 +}
 +
 +table.tabular
 +{
 +	margin: 1em auto;
 +	width: 80%;
 +}
 +
 +table.tabular td
 +{
 +	padding: 0.75em;
  }
\ No newline at end of file diff --git a/demos/time-tracker/index.php b/demos/time-tracker/index.php new file mode 100644 index 00000000..afada2c1 --- /dev/null +++ b/demos/time-tracker/index.php @@ -0,0 +1,20 @@ +<?php
 +
 +$basePath=dirname(__FILE__);
 +$frameworkPath='../../framework/prado.php';
 +$assetsPath=$basePath."/assets";
 +$runtimePath=$basePath."/protected/runtime";
 +
 +if(!is_file($frameworkPath))
 +	die("Unable to find prado framework path $frameworkPath.");
 +if(!is_writable($assetsPath))
 +	die("Please make sure that the directory $assetsPath is writable by Web server process.");
 +if(!is_writable($runtimePath))
 +	die("Please make sure that the directory $runtimePath is writable by Web server process.");
 +
 +require_once($frameworkPath);
 +
 +$application=new TApplication;
 +$application->run();
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/.htaccess b/demos/time-tracker/protected/.htaccess new file mode 100644 index 00000000..3418e55a --- /dev/null +++ b/demos/time-tracker/protected/.htaccess @@ -0,0 +1 @@ +deny from all
\ No newline at end of file diff --git a/demos/time-tracker/protected/APP_CODE/BaseDao.php b/demos/time-tracker/protected/APP_CODE/BaseDao.php new file mode 100644 index 00000000..f9146b59 --- /dev/null +++ b/demos/time-tracker/protected/APP_CODE/BaseDao.php @@ -0,0 +1,18 @@ +<?php
 +
 +class BaseDao
 +{
 +	private $_connection;
 +	
 +	public function setConnection($connection)
 +	{
 +		$this->_connection = $connection;
 +	}
 +	
 +	protected function getConnection()
 +	{
 +		return $this->_connection;
 +	}
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/APP_CODE/Project.php b/demos/time-tracker/protected/APP_CODE/Project.php new file mode 100644 index 00000000..ad9f7d19 --- /dev/null +++ b/demos/time-tracker/protected/APP_CODE/Project.php @@ -0,0 +1,17 @@ +<?php
 +
 +class Project
 +{
 +	public $ActualDuration = 0;
 +	public $CreatorUserName = '';
 +	public $CompletionDate = 0;
 +	public $DateCreated = 0;
 +	public $Description = '';
 +	public $EstimateDuration = 0;
 +	public $ID = 0;
 +	public $ManagerUserName = '';
 +	public $Name = '';
 +}
 +
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/APP_CODE/ProjectDao.php b/demos/time-tracker/protected/APP_CODE/ProjectDao.php new file mode 100644 index 00000000..25a2845d --- /dev/null +++ b/demos/time-tracker/protected/APP_CODE/ProjectDao.php @@ -0,0 +1,76 @@ +<?php
 +
 +Prado::using('Application.APP_CODE.BaseDao');
 +
 +class ProjectDao extends BaseDao
 +{
 +	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);			
 +		}
 +	} 
 +	
 +	public function getProjectByID($projectID)
 +	{
 +		$sqlmap = $this->getConnection();
 +		return $sqlmap->queryForObject('GetProjectByID', $projectID);
 +	}
 +	
 +	public function addUserToProject($project, $user)
 +	{
 +		$sqlmap = $this->getConnection();
 +		$project = $this->getProjectByID($project->ID);
 +		$user = $sqlmap->queryForObject('GetUserByName', $user->Name);
 +		$list = $sqlmap->queryForList('GetProjectMembers', $project);
 +		$userExists = false;
 +		foreach($list as $k)
 +		{
 +			if($k->ID == $user->ID)
 +				$userExists = true;
 +		}
 +		if(!$project)
 +		{
 +			throw new TimeTrackerException(
 +				'invalid_project', $project->Name);			
 +		}
 +		else if(!$user)
 +		{
 +			throw new TimeTrackerException(
 +				'invalid_user', $user->Name);	
 +		}
 +		else if($userExists)
 +		{
 +			throw new TimeTrackerException(
 +				'project_member_exists', $projet->Name, $user->Name);
 +		}
 +		else
 +		{
 +			$param['project'] = $project;
 +			$param['user'] = $user;
 +			return $sqlmap->insert('AddUserToProject', $param);
 +		}	
 +	}
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/APP_CODE/TimeTrackerException.php b/demos/time-tracker/protected/APP_CODE/TimeTrackerException.php new file mode 100644 index 00000000..d715eefa --- /dev/null +++ b/demos/time-tracker/protected/APP_CODE/TimeTrackerException.php @@ -0,0 +1,14 @@ +<?php
 +
 +class TimeTrackerException extends TException
 +{
 +	/**
 +	 * @return string path to the error message file
 +	 */
 +	protected function getErrorMessageFile()
 +	{
 +		return dirname(__FILE__).'/exceptions.txt';
 +	}	
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/APP_CODE/TimeTrackerUser.php b/demos/time-tracker/protected/APP_CODE/TimeTrackerUser.php new file mode 100644 index 00000000..4b6987bd --- /dev/null +++ b/demos/time-tracker/protected/APP_CODE/TimeTrackerUser.php @@ -0,0 +1,31 @@ +<?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');
 +	}
 +}
 +
 +class TimeTrackerUserException extends TimeTrackerException
 +{
 +	
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/APP_CODE/exceptions.txt b/demos/time-tracker/protected/APP_CODE/exceptions.txt new file mode 100644 index 00000000..e948f4d0 --- /dev/null +++ b/demos/time-tracker/protected/APP_CODE/exceptions.txt @@ -0,0 +1,3 @@ +timetracker_user_readonly_id 	= Time tracker user ID is read-only.
 +invalid_creator_and_manager		= Unable to find time tracker usernames '{1}' and '{2}' for project '{0}'.
 +project_exists					= Project '{0}' already exists.
\ No newline at end of file diff --git a/demos/time-tracker/protected/controls/Layout.php b/demos/time-tracker/protected/controls/Layout.php new file mode 100644 index 00000000..e612d52d --- /dev/null +++ b/demos/time-tracker/protected/controls/Layout.php @@ -0,0 +1,7 @@ +<?php
 +
 +class Layout extends TTemplateControl
 +{
 +
 +}
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/controls/Layout.tpl b/demos/time-tracker/protected/controls/Layout.tpl new file mode 100644 index 00000000..95c88fd8 --- /dev/null +++ b/demos/time-tracker/protected/controls/Layout.tpl @@ -0,0 +1,45 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
 +
 +<com:THead Title="PRADO Time Tracker Guide">
 +<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
 +<meta http-equiv="content-language" content="en"/>
 +</com:THead>
 +
 +<body>
 +<com:TForm>
 +<div id="header">
 +<div class="title">PRADO Time Tracker Guide</div>
 +<div class="image"></div>
 +</div>
 +
 +<com:TPanel ID="MainMenu" CssClass="mainmenu">
 +<a href="?">Home</a> |
 +<a href="http://www.pradosoft.com">PradoSoft.com</a> |
 +<a href="../../docs/quickstart.pdf">PDF Version</a> |
 +<com:THyperLink ID="PrinterLink" Text="Printer-friendly Version" />
 +</com:TPanel>
 +
 +<table width="100%" border="0" cellspacing="0" cellpadding="0">
 +<tr>
 +<td valign="top" width="1">
 +<com:Application.controls.TopicList ID="TopicPanel" />
 +</td>
 +<td valign="top">
 +<div id="content">
 +<com:TContentPlaceHolder ID="body" />
 +</div>
 +</td>
 +</tr>
 +</table>
 +
 +<div id="footer">
 +Copyright © 2005-2006 <a href="http://www.pradosoft.com">PradoSoft</a>.
 +<br/><br/>
 +
 +<a href="http://validator.w3.org/check?uri=referer">Valid XHTML 1.0 Transitional</a>
 +</div>
 +
 +</com:TForm>
 +</body>
 +</html>
\ No newline at end of file diff --git a/demos/time-tracker/protected/controls/TopicList.php b/demos/time-tracker/protected/controls/TopicList.php new file mode 100644 index 00000000..ce827cc0 --- /dev/null +++ b/demos/time-tracker/protected/controls/TopicList.php @@ -0,0 +1,8 @@ +<?php
 +
 +class TopicList extends TTemplateControl
 +{
 +
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/protected/controls/TopicList.tpl b/demos/time-tracker/protected/controls/TopicList.tpl new file mode 100644 index 00000000..bc9fff26 --- /dev/null +++ b/demos/time-tracker/protected/controls/TopicList.tpl @@ -0,0 +1,27 @@ +<div id="toc">
 +
 +<div class="topic">
 +<ul>
 +	<li><a href="?page=preface">Preface</a></li>
 +</ul>
 +<div>Prado Time Tracker Implementation Guide</div>
 +<ul>
 +	<li><a href="?page=Introduction">Introduction</a></li>
 +	<li><a href="?page=GettingStarted">Installation</a></li>
 +	<li><a href="?page=WritingUnitTest">Writing a Unit Test</a></li>
 +	<li><a href="?page=WritingFunctionalTest">Writing a Functional Web Test</a></li>
 +</ul>
 +
 +<div>Testing Business Code</div>
 +<ul>
 +	<li><a href="?page=CreateBusinessCode">Create Business Code</a></li>
 +	<li><a href="?page=UsingSQLMap">Using SQLMap Data Mapper</a></li>
 +	<li><a href="?page=UserClassAndExceptions">User Class and Exceptions</a></li>
 +	<li><a href="?page=MoreTests">More Tests</a></li>
 +</ul>
 +
 +</div>
 +
 +
 +
 +</div>
\ No newline at end of file diff --git a/demos/time-tracker/protected/data/time_tracker.db b/demos/time-tracker/protected/data/time_tracker.dbBinary files differ new file mode 100644 index 00000000..03fe9156 --- /dev/null +++ b/demos/time-tracker/protected/data/time_tracker.db diff --git a/demos/time-tracker/protected/pages/Docs/CreateBusinessCode.page b/demos/time-tracker/protected/pages/Docs/CreateBusinessCode.page new file mode 100644 index 00000000..e5afa572 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/CreateBusinessCode.page @@ -0,0 +1,127 @@ +<com:TContent ID="body">
 +<h1>Create Business Code</h1>
 +<p>We start the design with the database, the entity relationships are shown
 +in the diagram below.</p>
 +
 +<img src=<%~ db.png %> class="figure" />
 +
 +<p>Now we can begin to create and test some business code. Let us begin
 +with the <tt>Project</tt> defintions. First, we add some properties or fields.</p>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +<?php
 +class Project
 +{
 +	 public $ActualDuration = 0;
 +	 public $CreatorUserName = '';
 +	 public $CompletionDate = 0;
 +	 public $DateCreated = 0;
 +	 public $Description = '';
 +	 public $EstimateDuration = 0;
 +	 public $ID = 0;
 +	 public $ManagerUserName = '';
 +	 public $Name = '';
 +}
 +?>
 +</com:TTextHighlighter>
 +
 +<p>All the fields should be self explainatory. The <tt>ManagerUserName</tt>
 +is the user name of the project manager. Notice that the fields
 +are public, later on, we can change some or all of them to be private and
 +provide some accessor and mutators (i.e. getters and setters). If we want
 +we can let the <tt>Project</tt> class inherit <tt>TComponent</tt> such
 +that the getters and setters can be used like properties, such as those
 +found in Prado.</p>
 +
 +<h2>Business Services, test case first</h2>
 +<p>Next we want to add users to the project. For this, we start with some
 +unit tests. We are going to design the business logic around the concept of 
 +<a href="java">Data Access Objects</a> (DAO).</p> 
 +
 +<com:TTextHighlighter Language="php" CssClass="source">
 +<?php
 +Prado::using('Application.APP_CODE.*');
 +class ProjectDaoTestCase extends UnitTestCase
 +{
 +	function testProjectDaoCanCreateNewProject()
 +	{
 +		$project = new Project();
 +		$project->Name = "Project 1";
 +		
 +		$dao = new ProjectDao();
 +		
 +		$this->assertTrue($dao->createNewProject($project));
 +		$this->assertEqual($dao->getProjectByID(1), $project);
 +	}
 +}
 +?>
 +</com:TTextHighlighter>
 +<p>So what are we doing here? First we create a new <tt>Project</tt> named
 +"Project 1". Then we create a new <tt>ProjectDao</tt> so we can insert new projects
 +and retrieve it. We assert that a project will be create sucessfully using
 +<tt>assertTrue($do->createNewProject(...))</tt>. We also assert that
 +<tt>getProjectByID(1)</tt> will return an instance of <tt>Project</tt> class
 +with same data (the reference may be different).</p>
 +
 +<p>If we run the above unit test case, nothing is going to pass since we have
 +not even defined the <tt>ProjectDao</tt> class. So lets do that first and import
 +the class in the tests as well.</p>
 +
 +<p>We will create a base Dao class as follows, and we save as <tt>BaseDao.php</tt>
 +in our <tt>APP_CODE</tt> directory.</p>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +<?php
 +class BaseDao
 +{
 +	private $_connection;
 +	
 +	public function setConnection($connection)
 +	{
 +		$this->_connection = $connection;
 +	}
 +	
 +	protected function getConnection()
 +	{
 +		return $this->_connection;
 +	}
 +}
 +?>
 +</com:TTextHighlighter>
 +<p>And finally our <tt>ProjectDao</tt> class.</p>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +<?php
 +Prado::using('Application.APP_CODE.BaseDao');
 +class ProjectDao extends BaseDao
 +{
 +}
 +?>
 +</com:TTextHighlighter>
 +<p>If we run the unit test again, we get an error message something like
 +"<i>Fatal error: Call to undefined method ProjectDao::createNewProject() in ...</i>".
 +So let us, fix this.
 +</p>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +class ProjectDao extends BaseDao
 +{
 +	public function createNewProject($project)
 +	{		
 +	} 
 +	
 +	public function getProjectByID($projectID)
 +	{	
 +	}
 +}
 +</com:TTextHighlighter>
 +<p>Run the unit test again, we see a different type of error now. This time
 +the unit test runner complians that the test fails.</p>
 +<h2>Write Test, Run Test, Repeat</h2>
 +<div class="info">
 +<b>At this point, you may notice a repetition</b>
 +<ol>
 +	<li>write some test code,</li>
 +	<li>run the test, test gives an error,</li>
 +	<li>write some actual code to only remove the previous error,</li>
 +	<li>goto step 1 to repeat.</li>
 +</ol>
 +</div>
 +<p>We shall see how we can make this test pass in the next section.</p>
 +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/GettingStarted.page b/demos/time-tracker/protected/pages/Docs/GettingStarted.page new file mode 100644 index 00000000..8b8e8e5e --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/GettingStarted.page @@ -0,0 +1,67 @@ +<com:TContent ID="body"> +<h1>Installation</h1> +<p>There are a few pieces of tools and software needed throughout the development  +of the application. First we shall setup the environment for coding and testing. We +shall from here on assume that you have access to a working installation of a web server with +PHP and possibilly a MySQL database server. The first thing to do is install Prado and some +testing tools.</p> + +<h2>Download and Install Prado</h2> +<p>The minimum requirement by PRADO is that the Web server support PHP 5.  +	For the Time Tracker sample application, you need Prado version 3.1 or greater.</p> +<p>Installation of PRADO mainly involves downloading and unpacking.</p> +<ol> +	<li>Go to <a href="http://www.pradosoft.com/download/">pradosoft.com</a> to grab the latest version of PRADO.</li> +	<li>Unpack the PRADO release file to a Web-accessible directory.</li> +</ol> + +<p>You should at least first check that the demos bundled with the Prado distribution are +	working. See the <a href="../quickstart/index.php?page=GettingStarted.Installation"> +		quickstart tutorial for further instructions</a> on running the demos. +</p> + +<h2>Help! Nothing is working!</h2> +<p>If you encounter problems in downloading, unpacking and running the demo applications, please +	visit the <a href="http://www.pradosoft.com/forum/">forum to seek help</a>. Please +	do a search on the forum first or <a href="http://www.pradosoft.com/demos/quickstart/prado3_quick_start.pdf">read the PDF version</a> of the quickstart.  +	The friendly and wonderful people at the forum can better assist you if you can provide as much detail regarding your problem.  +	You should include in your post your server configuration details, the steps you took to reproduce your problem, and any error messages encountered.</p> + +<h2>Create a new Prado application</h2> +<p>Prado is bundled with a command line tool to create the necessary directory structure to run a hello world application. +The command tool <tt>prado-cli.php</tt> is a php script and can be found in the <tt>prado/framework/</tt> directory. +To create a new application, go to your document root and type the following command in console. +</p> +<com:TTextHighlighter Language="none" CssClass="source"> +php prado/framework/prado-cli.php -t -c time-tracker +</com:TTextHighlighter> + +<div class="tip"><b class="tip">Tip:</b> +For linux and OS X environments, you can <tt>chmod u+x prado-cli.php</tt> +and change the first line of <tt>prado-cli.php</tt> to the location of your +PHP command line interpreter. Now, you may call the script from command line +like an executable. +</div> + +<p>A directory named <tt>time-tracker</tt> will be created containing the following. +<com:TTextHighlighter Language="none" CssClass="source"> +document_root/time-tracker/assets/ +document_root/time-tracker/index.php +document_root/time-tracker/protected/ +document_root/time-tracker/protected/.htaccess +document_root/time-tracker/protected/pages/ +document_root/time-tracker/protected/pages/Home.page +document_root/time-tracker/protected/runtime/ +</com:TTextHighlighter> +<p>The <tt>time-tracker</tt> directory is where we are going to put all our code for the Time Tracker application.  +Since <tt>-t</tt> was passed into the <tt>prado-cli.php</tt> script, unit and functional test +	skeletons are also created.</p> +<com:TTextHighlighter Language="none" CssClass="source"> +document_root/time-tracker/tests/ +document_root/time-tracker/tests/unit/ +document_root/time-tracker/tests/functional/ +document_root/time-tracker/tests/unit.php +document_root/time-tracker/tests/functional.php +</com:TTextHighlighter> + +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/Home.page b/demos/time-tracker/protected/pages/Docs/Home.page new file mode 100644 index 00000000..5c7a486b --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/Home.page @@ -0,0 +1,10 @@ +<com:TContent ID="body"> +<h1>Prado Time Tracker Implementation Guide</h1> +<ul> +	<li><a href="?page=Introduction">Introduction</a></li> +	<li><a href="?page=GettingStarted">Getting Started</a></li> +	<li><a href="?page=WritingUnitTest">Writing a Unit Test</a></li> +	<li><a href="?page=WritingFunctionalTest">Writing a Functional Web Test</a></li> +	<li><a href="?page=CreateBusinessCode ">Create Business Code</a></li> +</ul> +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/Introduction.page b/demos/time-tracker/protected/pages/Docs/Introduction.page new file mode 100644 index 00000000..baa305f5 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/Introduction.page @@ -0,0 +1,64 @@ +<com:TContent ID="body"> +<h1>Introduction</h1> + +<p>The Time Tracker is a fully functional sample application designed to  +	introduce you to many of Prado's features. This guide is a step by step +	walkthrough starting from installation to deployment. The Time Tracker +	application is based on the  +	<a href="http://www.asp.net/downloads/starterkits/">ASP.NET's Time Tracker Starter Kit</a>. +</p> + +<h1>Time Tracker Overview</h1> +<p>The Time Tracker is a business web application for keeping track of hours spent +on a project, with ability to handle multiple resources as well as multiple projects.</p> +<h2>Basic Application Requirements</h2> +<p>The functional requirements of the Time Tracker is based on the  +	<a href="http://www.asp.net/downloads/starterkits/">ASP.NET's Time Tracker Starter Kit</a>  +	description.</p> +<p class="requirements"> +<h3>Create projects</h3> +<ul> +	<li>Define projection information like due dates, hours to complete, project +		resources, and more.</li> +	<li>Break down projects into tasks and track work on per-task basis.</li> +</ul> +<h3>Create and track tasks</h3> +<ul> +	<li>Track time spent each day by category and project.</li> +</ul> +<h3>Use reports to track progress</h3> +<ul> +	<li>Track overall progress across multiple projects, including estimated and actual work.</li> +	<li>Track total work for team resources across multiple projects.</li> +</ul> +</p> + +<h2>Technologies and Design Approached Demonstrated</h2> +<ul> +	<li>Prado event-driven component framework</li> +	<li>Separation of concerns: persistent storage, business logic, presentation</li> +	<li>Object-Relational mapping using SQLMap</li> +	<li>Unit testing and functional testing</li> +	<li>Promote code reuse</li> +</ul> + +<h1>Requirements</h1> +<p>The Time Tracker web application requires the following software and knowledge.</p> +<h2>Software requirements</h2> +<p>It is assumed that you are able to obtain and install the following pieces of software.</p> +<ol> +	<li>PHP version 5.0.4 or greater</li> +	<li>A web server, such as Apache, able to run PHP scripts</li> +	<li>MySQL database server version 4.1 or greater (alternatively SQLite or Postgres)</li> +	<li>Prado version 3.1 or later</li>	 +</ol> + +<p>In addition to software requirements, we assumed that you have some of the following knowledge.</p> +<ul> +	<li>Access the internet to download packaged code, and unzip these code.</li> +	<li>Understand the basic concepts of Object-Oriented programming in PHP such as +		classes, objects, methods, etc.</li> +	<li>Basic understanding of relational databases and Structured Query Language (SQL).</li> +	<li>Knows how to have fun, this is mandatory.</li> +</ul> +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/MoreTests.page b/demos/time-tracker/protected/pages/Docs/MoreTests.page new file mode 100644 index 00000000..5e598982 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/MoreTests.page @@ -0,0 +1,140 @@ +<com:TContent ID="body">
 +
 +<h1>Finishing up test case</h1>
 +<p>Up to this point, you should have the follow pieces of files and code.
 +<img src=<%~ project1.png %> class="figure" />
 +</p>
 +
 +<p>So what else can we test for at this point? A few reasonable
 +tests are to see what happens if the project already exists, and what if
 +the username does not exists.</p>
 +
 +<p>First we shall refactor our test code since much of the setup code 
 +for the mocked connection will need to be repeated in other test assertions.
 +We change the test case to have these three new methods.
 +</p>
 +
 +<com:TTextHighlighter Language="php" CssClass="source">
 +function setupMockConnectionFor($project)
 +{
 +	$customer = new TimeTrackerUser();
 +	$customer->ID = 1;
 +	$customer->Name = "Customer A";
 +	
 +	$manager = new TimeTrackerUser();
 +	$manager->ID = 2;
 +	$manager->Name = "Manager A";
 +	
 +	$conn = $this->connection;
 +	
 +	//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));	
 +}
 +
 +function createNewTestProject()
 +{
 +	$project = new Project();
 +	$project->Name = "Project 1";
 +	$project->CreatorUserName = "Customer A";
 +	$project->ManagerUserName = "Manager A";
 +	
 +	return $project;
 +}
 +
 +function assertProjectCreated($project)
 +{
 +	$this->assertTrue($this->dao->createNewProject($project));		
 +	$this->assertEqual($this->dao->getProjectByID(1), $project);
 +}
 +</com:TTextHighlighter>
 +
 +<p>Our refactored test method <tt>testProjectDaoCanCreateNewProject()</tt> 
 +is as follows.</p>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +function testProjectDaoCanCreateNewProject()
 +{
 +	$project = $this->createNewTestProject();
 +
 +	if(($conn = $this->connection) instanceof MockTSqlMapper)
 +	{
 +		$this->setupMockConnectionFor($project);
 +		$conn->expectMinimumCallCount('queryForObject', 3);
 +		$conn->expectAtLeastOnce('insert');
 +	}
 +	
 +	$this->assertProjectCreated($project);	
 +}
 +</com:TTextHighlighter>
 +
 +<p>To test that the project already exists, we modify the mock
 +connection and test for an exception.</p>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +function testProjectExistsException()
 +{
 +	$project = $this->createNewTestProject();
 +	
 +	if(($conn = $this->connection) instanceof MockTSqlMapper)
 +	{
 +		//make the project exist
 +		$conn->setReturnValue('queryForObject', 
 +				$project, array('GetProjectByName', 'Project 1'));
 +		$this->setupMockConnectionFor($project);
 +	}
 +	
 +	try
 +	{
 +		$this->assertProjectCreated($project);		
 +		$this->fail();
 +	}
 +	catch(TimeTrackerException $e)
 +	{
 +		$this->pass();
 +	}
 +}
 +</com:TTextHighlighter>
 +
 +<p>Other test method for testing missing customer and manager users
 +are done similarly. At this point, the test case file looks quite large.
 +We shall not add more tests to this case and we should rename the file 
 +from <tt>ProjectDaoTestCase.php</tt> to <tt>CreateNewProjectTestCase.php</tt>.
 +
 +<div class="note"><b class="tip">Note:</b>
 +A heirachical exception class may be more useful in testing for specific
 +exceptions. For simplicity, we decided to use only <tt>TimeTrackerException</tt>
 +for this small project.
 +</div>
 +
 +<div class="tip"><b class="tip">Tip:</b>Class, method and file naming is very
 +important to any project, the name should inform you at first glance what
 +is to be expected from the class, method or file. In addition, the naming
 +scheme should be uniform over the project.
 +</div>
 +
 +<div class="info"><b class="tip">Comment:</b>
 +These test may be too ridget as any changes to the implementation may
 +actually cause the tests to fail. This is the case with grey-box/white-box
 +testing, you are actually testing the implementation. Black box tests may
 +be more preferable, as it should only test the class interface (the method or 
 +function details such as parameters and may be return values). However, 
 +with the use of database connection to retrive data within the objects under test,
 +it may be more suitable to do intergration tests.
 +</div>
 +
 +</com:TContent>
 +
 diff --git a/demos/time-tracker/protected/pages/Docs/UserClassAndExceptions.page b/demos/time-tracker/protected/pages/Docs/UserClassAndExceptions.page new file mode 100644 index 00000000..f85e00be --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/UserClassAndExceptions.page @@ -0,0 +1,202 @@ +<com:TContent ID="body">
 +<h1>More complete ProjectTestCase</h1>
 +<p>
 +In creating a new project, we need to check the following:
 +<ul>
 +	<li>that the project does not already exists, i.e. no duplicate project name</li>
 +	<li>that the project creator and project manager exists when a new project is created</li>
 +</ul>
 +So to perform this task, we will modified the test code. 
 +<com:TTextHighlighter Language="php" CssClass="source">
 +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);
 +}
 +</com:TTextHighlighter>
 +</p>
 +<p>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.</p>
 +<div class="info"><b class="tip">Advantages of Mock</b>
 +<ol>
 +	<li>we don't need a real database base connection to test the code, 
 +	this means we can start relying on tested code ealier</li>
 +	<li>when a test fails we know that problem is not part of the database</li> 
 +	<li>when a test fail, we can quickly pin point the problem</li>
 +	<li>the test suite gives us the confidence to refactor our code</li>
 +</ol>
 +</div>
 +
 +<p>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.
 +</p>
 +
 +<p>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.</p>
 +
 +<p>If we run the above test, we will be faced with numerous errors. First will be
 +that the <tt>TimeTrackerUser</tt> can not be found.</p>
 +
 +<h1>Creating a new User Class</h1>
 +<p>Notice that the <tt>Project</tt> class contains <tt>CreatorUserName</tt> 
 +and <tt>ManagerUserName</tt> properties. So at some point we
 +are going to need at least one <tt>User</tt> class. We shall name the class as
 +<tt>TimeTrackerUser</tt> and save it as <tt>APP_CODE/TimeTrackerUser.php</tt>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +<?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');
 +	}
 +}
 +?>
 +</com:TTextHighlighter>
 +
 +<h1>Custom Exceptions</h1>
 +<p>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 <tt>framework/Exceptions/messages.txt</tt>. This
 +file location can be changed by overriding the <tt>getErrorMessageFile()</tt>
 +method of <tt>TException</tt> class. We define a custom exception class
 +for all Time Tracker application exceptions as <tt>TimeTrackerException</tt>
 +and save the class as <tt>APP_CODE/TimeTrackerException.php</tt>.</p>
 +
 +<com:TTextHighlighter Language="php" CssClass="source">
 +<?php
 +class TimeTrackerException extends TException
 +{
 +	/**
 +	 * @return string path to the error message file
 +	 */
 +	protected function getErrorMessageFile()
 +	{
 +        return dirname(__FILE__).'/exceptions.txt';
 +	}	
 +}
 +?>
 +</com:TTextHighlighter>
 +
 +<p>We then create a <tt>exceptions.txt</tt> file in the <tt>APP_CODE</tt>
 +directory with the following content.</p>
 +
 +<com:TTextHighlighter Language="text" CssClass="source">
 +timetracker_user_readonly_id 	= Time tracker user ID is read-only.
 +</com:TTextHighlighter>
 +
 +<p>Additional parameters passed in the exception constructor can be
 +added the message string using <tt>{0}</tt> as the first additional parameter,
 +and <tt>{1}</tt> as the second additional parameter, and so on.
 +For example, suppose we want to raise the follow exception.
 +</p> 
 +
 +<com:TTextHighlighter Language="php" CssClass="source">
 +throw new TimeTrackerException('project_exists', $projectName);
 +</com:TTextHighlighter>
 +
 +<p>The exception error message in <tt>exceptions.txt</tt> may contain something like:</p>
 +<com:TTextHighlighter Language="text" CssClass="source">
 +project_exists 	= Time tracker project '{0}' already exists.
 +</com:TTextHighlighter>
 +
 +<h1>Completing the test case</h1>
 +<p>From the unit test code, we can pretty much see what the implementation
 +for <tt>createNewProject()</tt> will look like.</p>
 +
 +<com:TTextHighlighter Language="php" CssClass="source">
 +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);         
 +	}
 +}
 +</com:TTextHighlighter>
 +
 +<div class="tip"><b class="tip">Tip:</b>
 +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
 +<tt>TimeTrackerException</tt> exception class for most exception cases. 
 +</div>
 +
 +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/UsingSQLMap.page b/demos/time-tracker/protected/pages/Docs/UsingSQLMap.page new file mode 100644 index 00000000..cc2abf4f --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/UsingSQLMap.page @@ -0,0 +1,210 @@ +<com:TContent ID="body">
 +<h1>Using SQLMap Data Mapper</h1>
 +<p>Before proceeding with write the code to pass the test in the previous section, we shall
 +make a design decision. We shall use SQLMap as our data access layer. Note
 +that <b>SQLMap is only available</b> offically in <b>Prado 3.1</b> or later.</p>
 +
 +<p>SQLMap is an PHP implemenation of an Object/Relational 
 +data mapper. SQLMap has the following basic features:
 +<ol>
 +	<li>collects all the Structured Query Language
 +(SQL) statements in an external XML file</li>
 +	<li>Maps data return from database queries into PHP objects</li>
 +	<li>Takes PHP objects as parameters in SQL queries</li>
 +</ol>
 +<p>SQLMap can be seen as a generic data mapper, rather than an
 +Object Relational Mapping (ORM) solution.</p>
 +
 +<p>The SQLMap API consists of the following methods. See the <a href="sqlmap">SQLMap manual</a> 
 +for further details.</p>
 +
 +<com:TTextHighlighter Language="php" CssClass="source">
 +/* Query API */
 +public function queryForObject($statementName, $parameter=null, $result=null);
 +public function queryForList($statementName, $parameter=null, $result=null,
 +                                $skip=-1, $max=-1);
 +public function queryForPagedList($statementName, $parameter=null, $pageSize=10);
 +public function queryForMap($statementName, $parameter=null,
 +                                $keyProperty=null, $valueProperty=null);
 +public function insert($statementName, $parameter=null)
 +public function update($statementName, $parameter=null)
 +public function delete($statementName, $parameter=null)
 +
 +/* Connection API */
 +public function openConnection()
 +public function closeConnection()
 +
 +/* Transaction API */
 +public function beginTransaction()
 +public function commitTransaction()
 +public function rollBackTransaction()
 +</com:TTextHighlighter>
 +
 +<h2>Fetch and Inserting Data Using SQLMap</h2>
 +<p>Back to our <tt>ProjectDao</tt> class, for testing we shall write the code
 +assuming we have an SQLMap client instance. The SQLMap client or connection
 +will be mocked or faked using a proxy. Later on, we shall extend our tests,
 +to become an intergration test, to include
 +a test database connection.</p>
 +
 +<h2>Creating a SQLMap connection</h2>
 +<p>We can test the <tt>ProjectDao</tt> using mock objects (see SimpleTest documentation
 +regarding mock objects) or using a real database connection. In PHP version
 +5.0.x, the sqlite database client is bundled by default. For version 5.1 or later
 +the sqlite database client is available as an extension. We shall use a sqlite
 +database to conduct our unit tests because the database can be restored to
 +the orginal state by just reloading the orginal database file.</p>
 +
 +<div class="info"><b class="tip">Comment:</b>
 +Unit tests using mocked objects may be too ridget as we need to mock
 +the connection object and thus the test is of the grey-box/white-box variaity.
 +That is, you are actually testing the implementation of the object under test
 +consideration. Thus, any changes to the implementation of the objects
 +under tests may actually cause the tests to fail. Black box tests may
 +be more preferable, as it should only test the class interface (the method or 
 +function details such as parameters and may be return values). A further 
 +disadvantage when using mocks for complex database connection is the complexity
 +required in setting the mocks for testing.
 +</div>
 +
 +<p>First, let us define the database table for projects.</p>
 +<com:TTextHighlighter Language="sql" CssClass="source">
 +Field               Type            Null
 +----------------------------------------
 +ProjectID           INTEGER         Yes
 +Name                VARCHAR(255)    No
 +Description         VARCHAR(255)    No
 +CreationDate        INT             No
 +Disabled            INT(1)          No
 +EstimateDuration    INT             No
 +CompletionDate      INT             No
 +CreatorID           INTEGER         No
 +ManagerID           INTEGER         Yes 
 +</com:TTextHighlighter>	
 +
 +<p>The corresponding SQLite query to create the table is given below.</p>
 +
 +<com:TTextHighlighter Language="sql" CssClass="source">
 +CREATE TABLE projects ( 
 +	ProjectID INTEGER PRIMARY KEY, 
 +	Name VARCHAR(255) NOT NULL, 
 +	Description VARCHAR(255) NOT NULL, 
 +	CreationDate INT NOT NULL, 
 +	Disabled INT(1) NOT NULL, 
 +	EstimateDuration INT NOT NULL, 
 +	CompletionDate INT NOT NULL, 
 +	CreatorID INTEGER NOT NULL, 
 +	ManagerID INTEGER 
 +);
 +CREATE UNIQUE INDEX projects_Name ON projects(Name);
 +CREATE INDEX project_name_index ON projects(Name);
 +</com:TTextHighlighter>
 +
 +<h2>Testing with Mocked Database Connection</h2>
 +<p>At this point, we have said nothing about databases. To create some unit tests
 +for the <tt>ProjectDao</tt> class, we are not going to use real database connections
 +but using a Mocked <tt>TSqlMapper</tt>. We modifiy the test as follows.
 +</p>
 +<com:TTextHighlighter Language="php" CssClass="source">
 +Prado::using('Application.APP_CODE.*');
 +Prado::using('System.DataAccess.SQLMap.TSqlMapper');
 +
 +Mock::generate('TSqlMapper');
 +
 +class ProjectDaoTestCase extends UnitTestCase
 +{
 +	protected $dao;
 +	protected $connection;
 +	
 +	function setup()
 +	{
 +		$this->dao= new ProjectDao();
 +		$this->connection = new MockTSqlMapper($this);
 +		$this->dao->setConnection($this->connection);
 +	}
 +
 +	function testProjectDaoCanCreateNewProject()
 +	{
 +		$project = new Project();
 +		$project->Name = "Project 1";
 +
 +		if(($conn = $this->connection) instanceof MockTSqlMapper)
 +		{
 +			$conn->expectOnce('insert', array('CreateNewProject', $project));
 +			$conn->setReturnValue('insert', true);
 +			
 +			$conn->expectOnce('queryForObject', array('GetProjectByID', 1));
 +			$conn->setReturnReference('queryForObject', $project);
 +		}
 +
 +		$this->assertTrue($this->dao->createNewProject($project));		
 +		$this->assertEqual($this->dao->getProjectByID(1), $project);
 +	}
 +}
 +</com:TTextHighlighter>
 +<p>The test code looks slight more complicated because later on we want to
 +test the same assertions against some real data.</p>
 +<p>
 +In the first two lines above, we simply import the business code and the <tt>TSqlMapper</tt> class.
 +We generate a <tt>MockTSqlMapper</tt>, a mock class or <tt>TSqlMapper</tt> using 
 +<tt>Mock::generate('TSqlMapper')</tt>. The method <tt>Mock::generate()</tt> is available from
 +the SimpleTest unit testing framework. An instance of <tt>MockTSqlMapper</tt>
 +will be used as our test connection for the DAO objects. This allows us to interogate the
 +internal workings of our <tt>ProjectDao</tt> class. 
 +</p>
 +
 +<p>In the <tt>setup()</tt> (this method is called before every test method), we create an instance of 
 +<tt>ProjectDao</tt> and set the connection to an instance of <tt>MockTSqlMapper</tt>.
 +</p>
 +
 +<h2>Testing internal workings of ProjectDao</h2>
 +
 +<p>So how do we test the internal workings of the ProjectDao <tt>createNewProject</tt> method?
 +First, we assume that the connection object will perform the correct database insertion and queries.
 +Then, we set the return value of the MockTSqlMapper instance to return what we have assumed. 
 +In addition, we expect that the TSqlMapper method <tt>queryForObject</tt> is called only once with 
 +the parameters we have assumed (e.g. <tt>$project</tt>). See the SimpleTest tutorial for further details regarding
 +unit testing with Mocks.
 +</p>
 +
 +<p>In our assertions and expectations, we have
 +<com:TTextHighlighter Language="php" CssClass="source">
 +$conn->expectOnce('insert', array('CreateNewProject', $project));
 +$conn->setReturnValue('insert', true);
 +
 +$this->assertTrue($this->dao->createNewProject($project));		
 +</com:TTextHighlighter>
 +This means that, we expect the <tt>createNewProject</tt> method in <tt>ProjectDao</tt>
 +to call the TSqlMapper method <tt>insert</tt> with parameter <tt>$project</tt> only once.
 +In addition, we <b>assume</b> that the returned value from the <tt>insert</tt> method of TSqlMapper
 +returns <tt>true</tt>. Finally, we test the <tt>createNewProject</tt> method with an assertion.
 +</p>
 +
 +<p>We now run the unit tests, we see that there are some failures or errors. Since we 
 +have not created any code in <tt>ProjectDao</tt> that performs what we want, the tests will fail. 
 +So lets make these test pass, we add some code to <tt>ProjectDao</tt> class. 
 +
 +<com:TTextHighlighter Language="php" CssClass="source">
 +class ProjectDao extends BaseDao
 +{
 +	public function createNewProject($project)
 +	{
 +		$sqlmap = $this->getConnection();
 +		return $sqlmap->insert('CreateNewProject', $project);	
 +	} 
 +	
 +	public function getProjectByID($projectID)
 +	{
 +		$sqlmap = $this->getConnection();
 +		return $sqlmap->queryForObject('GetProjectByID', $projectID);
 +	}
 +}
 +</com:TTextHighlighter>
 +</p>
 +
 +<p>If we run the unit tests again, we should see a green bar indicating that the tests have passed.
 +We can now proceed further and add more tests. Of course, the above test does not cover many
 +conditions, such as, <i>what happens if the project already exists?</i>, the details of these
 +tests is the subject of the next section. The full test suite can be found in the source.
 +</p>
 +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/WritingFunctionalTest.page b/demos/time-tracker/protected/pages/Docs/WritingFunctionalTest.page new file mode 100644 index 00000000..b4da0952 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/WritingFunctionalTest.page @@ -0,0 +1,39 @@ +<com:TContent ID="body"> +<h1>Writing a Functional Web Test</h1> +<p>In addition to unit testing, we shall also do some functional +testing or web testing. Functional tests are, in this case, basically automated tests that will  +interact with the overall web application, as if it was the user, while checking the output for correctness. +The functional test tool we shall use here is based on <a href="http://selenium.openqa.org/">Selenium</a> where the test cases can be written and run using PHP and SimpleTest. +</p> +<com:TTextHighlighter> +<?php +class HelloPradoTestCase extends SeleniumTestCase +{ +	function test() +	{ +		$this->open('../index.php'); +		$this->assertTextPresent('Welcome to Prado!'); +	} +} +?> +</com:TTextHighlighter> +<p>Save the code as <tt>HelloPradoTestCase.php</tt> in the <tt>document_root/time-tracker/tests/functional/</tt> +directory.</p> + +<p> +Functional test cases are written very similar to unit test cases. The method such as  +<tt>open($url)</tt> are those found in Selenium. All the methods available in Selenium are available. +</p> + +<h2>Run your first unit test case from your browser</h2> +<p>Point your browser to your development server's unit test case runner, e.g. +	<tt>http://web-server-address/time-tracker/tests/functional.php</tt>. You should see the following	 +	<img src="<%~ functional_test1.png %>" class="figure"/> +	<div class="caption"><b>Figure 4:</b> Functional test runner</div> +</p> +<p>Clicking on the <b>All</b> button, you should see +	<img src="<%~ functional_test2.png %>" class="figure"/> +	<div class="caption"><b>Figure 5:</b> Functional test success</div> +</p>	  + +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/WritingUnitTest.page b/demos/time-tracker/protected/pages/Docs/WritingUnitTest.page new file mode 100644 index 00000000..32c7bc79 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/WritingUnitTest.page @@ -0,0 +1,73 @@ +<com:TContent ID="body"> +<h1>Writing a Unit Test</h1> +<p>Unit testing is a useful tool when we want to start to test +	our individual business logic classes.   +	The <tt>tests/unit</tt> directory will be used to hold the unit test cases and <tt>tests/functional</tt> directory +to hold the function test cases. +</p> + +<h2>Write a unit test case</h2> +<p>We will start be writing a very simple unit test case.</p> +<com:TTextHighlighter Language="php" CssClass="source"> +<?php +class ProjectTestCase extends UnitTestCase +{ +	function testProjectClassExists() +	{ +		$project = new Project(); +		$this->pass(); +	} +} +?> +</com:TTextHighlighter> +<p>Save the code as <tt>ProjectTestCase.php</tt> in the <tt>document_root/time-tracker/tests/unit/</tt> +directory.</p> + +<h2>Run your first unit test case from your browser</h2> +<p>Point your browser to your development server's unit test case runner, e.g. +	<tt>http://web-server-address/time-tracker/tests/unit.php</tt>. You should see the following	 +<img src="<%~ unit_test1.png %>" class="figure"/> +<div class="caption"><b>Figure 1:</b> Unit test runner</div> +</p> +<p>Clicking on the <tt>ProjectTestCase.php</tt> like, you should see  +<img src="<%~ unit_test2.png %>" class="figure"/> +<div class="caption"><b>Figure 2:</b> Unit test failure</div> +</p> + +<h2>Smallest step to make the test pass.</h2> + +<p>Obviously, we need create the class <tt>Project</tt>, so lets define the class.</p> +<com:TTextHighlighter Language="php" CssClass="source"> +<?php +class Project +{ +} +?> +</com:TTextHighlighter> +<p>Save the above code as <tt>time-tracker/protected/pages/APP_CODE/Project.php</tt>. +	Where the <tt>APP_CODE</tt> directory will contain most of your business logic code for the Time Tracker application.</p> +<p>We also need to add the following line in our test case so as to include the <tt>Project</tt> class file when running the tests.</p> + +<p class="note"> +The statement <tt>Prado::using('Application.APP_CODE.Project')</tt> basically  +loads the <tt>Project.php</tt> class file. It assumes that a class name <tt>Project</tt> has filename <tt>Project.php</tt>. +For futher details regarding <tt>Prado::using</tt> can be found in <a href="http://www.pradosoft.com/demos/quickstart/index.php?page=Fundamentals.Components#704">Prado Namespaces</a> documentation. +</p> + +<com:TTextHighlighter Language="php" CssClass="source"> +<?php +Prado::using('Application.APP_CODE.Project'); +class ProjectTestCase extends UnitTestCase +{ +	... +} +?> +</com:TTextHighlighter> +<p>Run the unit test runner again, we see that the test has passed. +	<img src="<%~ unit_test3.png %>" class="figure"/> +	<div class="caption"><b>Figure 3:</b> Unit test success</div> +</p>	 +<p> +Later on, we shall write more test cases. See the SimpleTest documentation for detailed tutorial on writing test cases.</p> + +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/config.xml b/demos/time-tracker/protected/pages/Docs/config.xml new file mode 100644 index 00000000..e0850c2c --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/config.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?>
 +
 +<configuration>
 +  <paths>
 +    <alias id="Pages" path="." />
 +  </paths>
 +  <pages MasterClass="Application.controls.Layout" Theme="PradoSoft" />
 +</configuration>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/db.png b/demos/time-tracker/protected/pages/Docs/db.pngBinary files differ new file mode 100644 index 00000000..f2209ef4 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/db.png diff --git a/demos/time-tracker/protected/pages/Docs/functional_test1.png b/demos/time-tracker/protected/pages/Docs/functional_test1.pngBinary files differ new file mode 100644 index 00000000..33908734 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/functional_test1.png diff --git a/demos/time-tracker/protected/pages/Docs/functional_test2.png b/demos/time-tracker/protected/pages/Docs/functional_test2.pngBinary files differ new file mode 100644 index 00000000..fb507e72 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/functional_test2.png diff --git a/demos/time-tracker/protected/pages/Docs/preface.page b/demos/time-tracker/protected/pages/Docs/preface.page new file mode 100644 index 00000000..fa0ba5c3 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/preface.page @@ -0,0 +1,19 @@ +<com:TContent ID="body">
 +<h1>Prado Time Tracker</h1>
 +<p>This documentation is complete walk-through guide detailing the
 +steps involved in implementating of the Prado Time Tracker web application.
 +</p>
 +<h2>Target Audience</h2>
 +<p>The guide is intended for readers who has some experience
 +using PHP version 5 and some basic concepts of Object-Oriented application
 +design. The guide will assume that the reader understands the primary concepts
 +of class, methods, class inheritance, and other basic features offered by PHP
 +version 5 or later.</p>
 +<h2>Questions and Comments</h2>
 +<p>Readers are encouraged to ask questions as the forum regarding any
 +particular aspect of this documentation. Comment and constructive criticisms
 +are most welcome. Questions and comments may be directed at
 +<a href="http://www.pradosoft.com/forum/">http://www.pradosoft.com/forum/</a>.
 +</p>
 +
 +</com:TContent>
\ No newline at end of file diff --git a/demos/time-tracker/protected/pages/Docs/project1.png b/demos/time-tracker/protected/pages/Docs/project1.pngBinary files differ new file mode 100644 index 00000000..a250a943 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/project1.png diff --git a/demos/time-tracker/protected/pages/Docs/unit_test1.png b/demos/time-tracker/protected/pages/Docs/unit_test1.pngBinary files differ new file mode 100644 index 00000000..66b62e19 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/unit_test1.png diff --git a/demos/time-tracker/protected/pages/Docs/unit_test2.png b/demos/time-tracker/protected/pages/Docs/unit_test2.pngBinary files differ new file mode 100644 index 00000000..e33544d2 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/unit_test2.png diff --git a/demos/time-tracker/protected/pages/Docs/unit_test3.png b/demos/time-tracker/protected/pages/Docs/unit_test3.pngBinary files differ new file mode 100644 index 00000000..bbc04551 --- /dev/null +++ b/demos/time-tracker/protected/pages/Docs/unit_test3.png diff --git a/demos/time-tracker/tests/functional.php b/demos/time-tracker/tests/functional.php new file mode 100644 index 00000000..c216ada8 --- /dev/null +++ b/demos/time-tracker/tests/functional.php @@ -0,0 +1,10 @@ +<?php
 +
 +include_once '../../prado-trunk\tests\test_tools\functional_tests.php';
 +
 +$test_cases = dirname(__FILE__)."/functional";
 +
 +$tester=new PradoFunctionalTester($test_cases);
 +$tester->run(new SimpleReporter());
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/tests/functional/HelloPradoTestCase.php b/demos/time-tracker/tests/functional/HelloPradoTestCase.php new file mode 100644 index 00000000..b9e4f1e3 --- /dev/null +++ b/demos/time-tracker/tests/functional/HelloPradoTestCase.php @@ -0,0 +1,14 @@ +<?php
 +
 +//web testing
 +class HelloPradoTestCase extends SeleniumTestCase
 +{
 +	function testIndexPage()
 +	{
 +		$this->open('../index.php');
 +		$this->assertTextPresent('Welcome to Prado!');
 +		//add more test assertions...
 +	}
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/tests/unit.php b/demos/time-tracker/tests/unit.php new file mode 100644 index 00000000..a920b205 --- /dev/null +++ b/demos/time-tracker/tests/unit.php @@ -0,0 +1,11 @@ +<?php
 +
 +include_once '../../prado-trunk/tests/test_tools/unit_tests.php';
 +
 +$app_directory = "../protected";
 +$test_cases = dirname(__FILE__)."/unit";
 +
 +$tester = new PradoUnitTester($test_cases, $app_directory);
 +$tester->run(new HtmlReporter());
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/tests/unit/AddUserToProjectTestCase.php b/demos/time-tracker/tests/unit/AddUserToProjectTestCase.php new file mode 100644 index 00000000..36defc57 --- /dev/null +++ b/demos/time-tracker/tests/unit/AddUserToProjectTestCase.php @@ -0,0 +1,35 @@ +<?php
 +
 +require_once(dirname(__FILE__).'/ProjectDaoTestCase.php');
 +
 +class AddUserToProjectTestCase extends ProjectDaoTestCase
 +{		
 +	function testCanAddNewUserToProject()
 +	{
 +		$project = $this->createNewTestProject();
 +		
 +		$user = new TimeTrackerUser();
 +		$user->ID = 3;
 +		$user->Name = "test user 1";
 +		
 +		if(($conn = $this->connection) instanceof MockTSqlMapper)
 +		{
 +			$this->setupMockConnectionFor($project);
 +			$conn->setReturnReference('queryForObject', $user, array('GetUserByName', $user->Name));
 +			$conn->setReturnValue('queryForList', array(), array('GetProjectMembers', $project));
 +			
 +			$param['project'] = $project;
 +			$param['user'] = $user;
 +			
 +			$conn->setReturnValue('insert', true, array('AddNewUserToProject', $param));
 +			
 +			$conn->expectAtLeastOnce('insert');
 +			$conn->expectAtLeastOnce('queryForList'); 
 +		}
 +		
 +		$this->assertTrue($this->dao->createNewProject($project));
 +		$this->assertTrue($this->dao->addUserToProject($project, $user));
 +	}
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/tests/unit/CreateNewProjectTestCase.php b/demos/time-tracker/tests/unit/CreateNewProjectTestCase.php new file mode 100644 index 00000000..0aa67405 --- /dev/null +++ b/demos/time-tracker/tests/unit/CreateNewProjectTestCase.php @@ -0,0 +1,90 @@ +<?php
 +
 +require_once(dirname(__FILE__).'/ProjectDaoTestCase.php');
 +
 +class CreateNewProjectTestCase extends ProjectDaoTestCase
 +{
 +	function testProjectDaoCanCreateNewProject()
 +	{
 +		$project = $this->createNewTestProject();
 +	
 +		if(($conn = $this->connection) instanceof MockTSqlMapper)
 +		{
 +			$this->setupMockConnectionFor($project);
 +			$conn->expectMinimumCallCount('queryForObject', 3);
 +			$conn->expectAtLeastOnce('insert');
 +		}
 +		
 +		$this->assertProjectCreated($project);	
 +	}
 +	
 +	function testProjectExistsException()
 +	{
 +		$project = $this->createNewTestProject();
 +		
 +		if(($conn = $this->connection) instanceof MockTSqlMapper)
 +		{
 +			//make the project exist
 +			$conn->setReturnValue('queryForObject', 
 +					$project, array('GetProjectByName', 'Project 1'));
 +			$this->setupMockConnectionFor($project);
 +		}
 +		
 +		try
 +		{
 +			$this->assertProjectCreated($project);		
 +			$this->fail();
 +		}
 +		catch(TimeTrackerException $e)
 +		{
 +			$this->pass();
 +		}
 +	}
 +	function testProjectCustomerNotExistsException()
 +	{
 +		$project = $this->createNewTestProject();
 +		
 +		if(($conn = $this->connection) instanceof MockTSqlMapper)
 +		{
 +			//customer does not exist
 +			$conn->setReturnValue('queryForObject', 
 +					null, array('GetUserByName', 'Customer A'));
 +			$this->setupMockConnectionFor($project);	
 +		}
 +		
 +		try
 +		{
 +			$this->assertProjectCreated($project);
 +			$this->fail();
 +		}
 +		catch(TimeTrackerException $e)
 +		{
 +			$this->pass();
 +		}
 +	}
 +	
 +	function testProjectManagerNotExistsException()
 +	{
 +		$project = $this->createNewTestProject();
 +		
 +		if(($conn = $this->connection) instanceof MockTSqlMapper)
 +		{
 +			//manager does not exist
 +			$conn->setReturnValue('queryForObject', 
 +					null, array('GetUserByName', 'Manager A'));
 +			$this->setupMockConnectionFor($project);
 +		}
 +		
 +		try
 +		{
 +			$this->assertProjectCreated($project);
 +			$this->fail();
 +		}
 +		catch(TimeTrackerException $e)
 +		{
 +			$this->pass();
 +		}
 +	}
 +}
 +
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/tests/unit/ProjectDaoTestCase.php b/demos/time-tracker/tests/unit/ProjectDaoTestCase.php new file mode 100644 index 00000000..bc576630 --- /dev/null +++ b/demos/time-tracker/tests/unit/ProjectDaoTestCase.php @@ -0,0 +1,92 @@ +<?php
 +
 +//formerly PradoDaoTestCase.php
 +
 +Prado::using('Application.APP_CODE.*');
 +Prado::using('System.DataAccess.SQLMap.TSqlMapper');
 +
 +Mock::generate('TSqlMapper');
 +
 +class ProjectDaoTestCase extends UnitTestCase
 +{
 +	protected $dao;
 +	protected $connection;
 +	
 +	function setup()
 +	{
 +		$this->dao= new ProjectDao();
 +		$this->connection = new MockTSqlMapper($this);
 +		$this->dao->setConnection($this->connection);
 +	}
 +
 +/*
 +  	//Simple test case, will not detect project existanc
 +  	//This case will clash with the more complete test case below. 
 +	function testProjectDaoCanCreateNewProject()
 +	{
 +		$project = new Project();
 +		$project->Name = "Project 1";
 + 
 +		if(($conn = $this->connection) instanceof MockTSqlMapper)
 +		{
 +			$conn->expectOnce('insert', array('CreateNewProject', $project));
 +			$conn->setReturnValue('insert', true);
 +			
 +			$conn->expectOnce('queryForObject', array('GetProjectByID', 1));
 +			$conn->setReturnReference('queryForObject', $project);
 +		}
 + 
 +		$this->assertTrue($this->dao->createNewProject($project));		
 +		$this->assertEqual($this->dao->getProjectByID(1), $project);
 +	}
 +*/
 +	function setupMockConnectionFor($project)
 +	{
 +		$customer = new TimeTrackerUser();
 +		$customer->ID = 1;
 +		$customer->Name = "Customer A";
 +		
 +		$manager = new TimeTrackerUser();
 +		$manager->ID = 2;
 +		$manager->Name = "Manager A";
 +		
 +		$conn = $this->connection;
 +		
 +		//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));	
 +	}
 +	
 +	function createNewTestProject()
 +	{
 +		$project = new Project();
 +		$project->Name = "Project 1";
 +		$project->CreatorUserName = "Customer A";
 +		$project->ManagerUserName = "Manager A";
 +		
 +		return $project;
 +	}
 +
 +	function assertProjectCreated($project)
 +	{
 +		$this->assertTrue($this->dao->createNewProject($project));		
 +		$this->assertEqual($this->dao->getProjectByID(1), $project);
 +	}
 +	
 +}
 +?>
\ No newline at end of file diff --git a/demos/time-tracker/tests/unit/ProjectTestCase.php b/demos/time-tracker/tests/unit/ProjectTestCase.php new file mode 100644 index 00000000..a61c4f77 --- /dev/null +++ b/demos/time-tracker/tests/unit/ProjectTestCase.php @@ -0,0 +1,15 @@ +<?php
 +
 +//import Project class.
 +Prado::using('Application.APP_CODE.Project');
 +
 +class ProjectTestCase extends UnitTestCase
 +{
 +	function testProjectClassExists()
 +	{
 +		$project = new Project();
 +		$this->pass();
 +	}
 +}
 +
 +?>
\ No newline at end of file | 
