From 143980b6dab8ad87c44518e5b7befb614fb83b85 Mon Sep 17 00:00:00 2001 From: wei <> Date: Fri, 14 Jul 2006 06:56:16 +0000 Subject: Add time-tracker sample and docs. (Incomplete) --- demos/time-tracker/protected/.htaccess | 1 + demos/time-tracker/protected/APP_CODE/BaseDao.php | 18 ++ demos/time-tracker/protected/APP_CODE/Project.php | 17 ++ .../time-tracker/protected/APP_CODE/ProjectDao.php | 76 ++++++++ .../protected/APP_CODE/TimeTrackerException.php | 14 ++ .../protected/APP_CODE/TimeTrackerUser.php | 31 +++ .../time-tracker/protected/APP_CODE/exceptions.txt | 3 + demos/time-tracker/protected/controls/Layout.php | 7 + demos/time-tracker/protected/controls/Layout.tpl | 45 +++++ .../time-tracker/protected/controls/TopicList.php | 8 + .../time-tracker/protected/controls/TopicList.tpl | 27 +++ demos/time-tracker/protected/data/time_tracker.db | Bin 0 -> 16384 bytes .../protected/pages/Docs/CreateBusinessCode.page | 127 +++++++++++++ .../protected/pages/Docs/GettingStarted.page | 67 +++++++ demos/time-tracker/protected/pages/Docs/Home.page | 10 + .../protected/pages/Docs/Introduction.page | 64 +++++++ .../protected/pages/Docs/MoreTests.page | 140 ++++++++++++++ .../pages/Docs/UserClassAndExceptions.page | 202 ++++++++++++++++++++ .../protected/pages/Docs/UsingSQLMap.page | 210 +++++++++++++++++++++ .../pages/Docs/WritingFunctionalTest.page | 39 ++++ .../protected/pages/Docs/WritingUnitTest.page | 73 +++++++ demos/time-tracker/protected/pages/Docs/config.xml | 8 + demos/time-tracker/protected/pages/Docs/db.png | Bin 0 -> 26879 bytes .../protected/pages/Docs/functional_test1.png | Bin 0 -> 92502 bytes .../protected/pages/Docs/functional_test2.png | Bin 0 -> 104726 bytes .../time-tracker/protected/pages/Docs/preface.page | 19 ++ .../time-tracker/protected/pages/Docs/project1.png | Bin 0 -> 338441 bytes .../protected/pages/Docs/unit_test1.png | Bin 0 -> 55855 bytes .../protected/pages/Docs/unit_test2.png | Bin 0 -> 87439 bytes .../protected/pages/Docs/unit_test3.png | Bin 0 -> 61923 bytes 30 files changed, 1206 insertions(+) create mode 100644 demos/time-tracker/protected/.htaccess create mode 100644 demos/time-tracker/protected/APP_CODE/BaseDao.php create mode 100644 demos/time-tracker/protected/APP_CODE/Project.php create mode 100644 demos/time-tracker/protected/APP_CODE/ProjectDao.php create mode 100644 demos/time-tracker/protected/APP_CODE/TimeTrackerException.php create mode 100644 demos/time-tracker/protected/APP_CODE/TimeTrackerUser.php create mode 100644 demos/time-tracker/protected/APP_CODE/exceptions.txt create mode 100644 demos/time-tracker/protected/controls/Layout.php create mode 100644 demos/time-tracker/protected/controls/Layout.tpl create mode 100644 demos/time-tracker/protected/controls/TopicList.php create mode 100644 demos/time-tracker/protected/controls/TopicList.tpl create mode 100644 demos/time-tracker/protected/data/time_tracker.db create mode 100644 demos/time-tracker/protected/pages/Docs/CreateBusinessCode.page create mode 100644 demos/time-tracker/protected/pages/Docs/GettingStarted.page create mode 100644 demos/time-tracker/protected/pages/Docs/Home.page create mode 100644 demos/time-tracker/protected/pages/Docs/Introduction.page create mode 100644 demos/time-tracker/protected/pages/Docs/MoreTests.page create mode 100644 demos/time-tracker/protected/pages/Docs/UserClassAndExceptions.page create mode 100644 demos/time-tracker/protected/pages/Docs/UsingSQLMap.page create mode 100644 demos/time-tracker/protected/pages/Docs/WritingFunctionalTest.page create mode 100644 demos/time-tracker/protected/pages/Docs/WritingUnitTest.page create mode 100644 demos/time-tracker/protected/pages/Docs/config.xml create mode 100644 demos/time-tracker/protected/pages/Docs/db.png create mode 100644 demos/time-tracker/protected/pages/Docs/functional_test1.png create mode 100644 demos/time-tracker/protected/pages/Docs/functional_test2.png create mode 100644 demos/time-tracker/protected/pages/Docs/preface.page create mode 100644 demos/time-tracker/protected/pages/Docs/project1.png create mode 100644 demos/time-tracker/protected/pages/Docs/unit_test1.png create mode 100644 demos/time-tracker/protected/pages/Docs/unit_test2.png create mode 100644 demos/time-tracker/protected/pages/Docs/unit_test3.png (limited to 'demos/time-tracker/protected') 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 @@ +_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 @@ + \ 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 @@ +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 @@ + \ 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 @@ +_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 @@ + \ 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 @@ + + + + + + + + + + + + + +Home | +PradoSoft.com | +PDF Version | + + + + + + + + +
+ + +
+ +
+
+ + + +
+ + \ 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 @@ + \ 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 @@ +
+ +
+ +
Prado Time Tracker Implementation Guide
+ + +
Testing Business Code
+ + +
+ + + +
\ 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.db new file mode 100644 index 00000000..03fe9156 Binary files /dev/null and b/demos/time-tracker/protected/data/time_tracker.db differ 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 @@ + +

Create Business Code

+

We start the design with the database, the entity relationships are shown +in the diagram below.

+ + class="figure" /> + +

Now we can begin to create and test some business code. Let us begin +with the Project defintions. First, we add some properties or fields.

+ +<?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 = ''; +} +?> + + +

All the fields should be self explainatory. The ManagerUserName +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 Project class inherit TComponent such +that the getters and setters can be used like properties, such as those +found in Prado.

+ +

Business Services, test case first

+

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 +Data Access Objects (DAO).

+ + +<?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); + } +} +?> + +

So what are we doing here? First we create a new Project named +"Project 1". Then we create a new ProjectDao so we can insert new projects +and retrieve it. We assert that a project will be create sucessfully using +assertTrue($do->createNewProject(...)). We also assert that +getProjectByID(1) will return an instance of Project class +with same data (the reference may be different).

+ +

If we run the above unit test case, nothing is going to pass since we have +not even defined the ProjectDao class. So lets do that first and import +the class in the tests as well.

+ +

We will create a base Dao class as follows, and we save as BaseDao.php +in our APP_CODE directory.

+ +<?php +class BaseDao +{ + private $_connection; + + public function setConnection($connection) + { + $this->_connection = $connection; + } + + protected function getConnection() + { + return $this->_connection; + } +} +?> + +

And finally our ProjectDao class.

+ +<?php +Prado::using('Application.APP_CODE.BaseDao'); +class ProjectDao extends BaseDao +{ +} +?> + +

If we run the unit test again, we get an error message something like +"Fatal error: Call to undefined method ProjectDao::createNewProject() in ...". +So let us, fix this. +

+ +class ProjectDao extends BaseDao +{ + public function createNewProject($project) + { + } + + public function getProjectByID($projectID) + { + } +} + +

Run the unit test again, we see a different type of error now. This time +the unit test runner complians that the test fails.

+

Write Test, Run Test, Repeat

+
+At this point, you may notice a repetition +
    +
  1. write some test code,
  2. +
  3. run the test, test gives an error,
  4. +
  5. write some actual code to only remove the previous error,
  6. +
  7. goto step 1 to repeat.
  8. +
+
+

We shall see how we can make this test pass in the next section.

+
\ 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 @@ + +

Installation

+

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.

+ +

Download and Install Prado

+

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.

+

Installation of PRADO mainly involves downloading and unpacking.

+
    +
  1. Go to pradosoft.com to grab the latest version of PRADO.
  2. +
  3. Unpack the PRADO release file to a Web-accessible directory.
  4. +
+ +

You should at least first check that the demos bundled with the Prado distribution are + working. See the + quickstart tutorial for further instructions on running the demos. +

+ +

Help! Nothing is working!

+

If you encounter problems in downloading, unpacking and running the demo applications, please + visit the forum to seek help. Please + do a search on the forum first or read the PDF version 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.

+ +

Create a new Prado application

+

Prado is bundled with a command line tool to create the necessary directory structure to run a hello world application. +The command tool prado-cli.php is a php script and can be found in the prado/framework/ directory. +To create a new application, go to your document root and type the following command in console. +

+ +php prado/framework/prado-cli.php -t -c time-tracker + + +
Tip: +For linux and OS X environments, you can chmod u+x prado-cli.php +and change the first line of prado-cli.php to the location of your +PHP command line interpreter. Now, you may call the script from command line +like an executable. +
+ +

A directory named time-tracker will be created containing the following. + +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/ + +

The time-tracker directory is where we are going to put all our code for the Time Tracker application. +Since -t was passed into the prado-cli.php script, unit and functional test + skeletons are also created.

+ +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 + + +
\ 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 @@ + +

Prado Time Tracker Implementation Guide

+ +
\ 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 @@ + +

Introduction

+ +

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 + ASP.NET's Time Tracker Starter Kit. +

+ +

Time Tracker Overview

+

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.

+

Basic Application Requirements

+

The functional requirements of the Time Tracker is based on the + ASP.NET's Time Tracker Starter Kit + description.

+

+

Create projects

+ +

Create and track tasks

+ +

Use reports to track progress

+ +

+ +

Technologies and Design Approached Demonstrated

+ + +

Requirements

+

The Time Tracker web application requires the following software and knowledge.

+

Software requirements

+

It is assumed that you are able to obtain and install the following pieces of software.

+
    +
  1. PHP version 5.0.4 or greater
  2. +
  3. A web server, such as Apache, able to run PHP scripts
  4. +
  5. MySQL database server version 4.1 or greater (alternatively SQLite or Postgres)
  6. +
  7. Prado version 3.1 or later
  8. +
+ +

In addition to software requirements, we assumed that you have some of the following knowledge.

+ +
\ 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 @@ + + +

Finishing up test case

+

Up to this point, you should have the follow pieces of files and code. + class="figure" /> +

+ +

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.

+ +

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. +

+ + +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); +} + + +

Our refactored test method testProjectDaoCanCreateNewProject() +is as follows.

+ +function testProjectDaoCanCreateNewProject() +{ + $project = $this->createNewTestProject(); + + if(($conn = $this->connection) instanceof MockTSqlMapper) + { + $this->setupMockConnectionFor($project); + $conn->expectMinimumCallCount('queryForObject', 3); + $conn->expectAtLeastOnce('insert'); + } + + $this->assertProjectCreated($project); +} + + +

To test that the project already exists, we modify the mock +connection and test for an exception.

+ +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(); + } +} + + +

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 ProjectDaoTestCase.php to CreateNewProjectTestCase.php. + +

Note: +A heirachical exception class may be more useful in testing for specific +exceptions. For simplicity, we decided to use only TimeTrackerException +for this small project. +
+ +
Tip: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. +
+ +
Comment: +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. +
+ +
+ 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 @@ + +

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. +
  3. when a test fails we know that problem is not part of the database
  4. +
  5. when a test fail, we can quickly pin point the problem
  6. +
  7. the test suite gives us the confidence to refactor our code
  8. +
+
+ +

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. +
+ +
\ 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 @@ + +

Using SQLMap Data Mapper

+

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 SQLMap is only available offically in Prado 3.1 or later.

+ +

SQLMap is an PHP implemenation of an Object/Relational +data mapper. SQLMap has the following basic features: +

    +
  1. collects all the Structured Query Language +(SQL) statements in an external XML file
  2. +
  3. Maps data return from database queries into PHP objects
  4. +
  5. Takes PHP objects as parameters in SQL queries
  6. +
+

SQLMap can be seen as a generic data mapper, rather than an +Object Relational Mapping (ORM) solution.

+ +

The SQLMap API consists of the following methods. See the SQLMap manual +for further details.

+ + +/* 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() + + +

Fetch and Inserting Data Using SQLMap

+

Back to our ProjectDao 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.

+ +

Creating a SQLMap connection

+

We can test the ProjectDao 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.

+ +
Comment: +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. +
+ +

First, let us define the database table for projects.

+ +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 + + +

The corresponding SQLite query to create the table is given below.

+ + +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); + + +

Testing with Mocked Database Connection

+

At this point, we have said nothing about databases. To create some unit tests +for the ProjectDao class, we are not going to use real database connections +but using a Mocked TSqlMapper. We modifiy the test as follows. +

+ +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); + } +} + +

The test code looks slight more complicated because later on we want to +test the same assertions against some real data.

+

+In the first two lines above, we simply import the business code and the TSqlMapper class. +We generate a MockTSqlMapper, a mock class or TSqlMapper using +Mock::generate('TSqlMapper'). The method Mock::generate() is available from +the SimpleTest unit testing framework. An instance of MockTSqlMapper +will be used as our test connection for the DAO objects. This allows us to interogate the +internal workings of our ProjectDao class. +

+ +

In the setup() (this method is called before every test method), we create an instance of +ProjectDao and set the connection to an instance of MockTSqlMapper. +

+ +

Testing internal workings of ProjectDao

+ +

So how do we test the internal workings of the ProjectDao createNewProject 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 queryForObject is called only once with +the parameters we have assumed (e.g. $project). See the SimpleTest tutorial for further details regarding +unit testing with Mocks. +

+ +

In our assertions and expectations, we have + +$conn->expectOnce('insert', array('CreateNewProject', $project)); +$conn->setReturnValue('insert', true); + +$this->assertTrue($this->dao->createNewProject($project)); + +This means that, we expect the createNewProject method in ProjectDao +to call the TSqlMapper method insert with parameter $project only once. +In addition, we assume that the returned value from the insert method of TSqlMapper +returns true. Finally, we test the createNewProject method with an assertion. +

+ +

We now run the unit tests, we see that there are some failures or errors. Since we +have not created any code in ProjectDao that performs what we want, the tests will fail. +So lets make these test pass, we add some code to ProjectDao class. + + +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); + } +} + +

+ +

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, what happens if the project already exists?, the details of these +tests is the subject of the next section. The full test suite can be found in the source. +

+
\ 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 @@ + +

Writing a Functional Web Test

+

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 Selenium where the test cases can be written and run using PHP and SimpleTest. +

+ +<?php +class HelloPradoTestCase extends SeleniumTestCase +{ + function test() + { + $this->open('../index.php'); + $this->assertTextPresent('Welcome to Prado!'); + } +} +?> + +

Save the code as HelloPradoTestCase.php in the document_root/time-tracker/tests/functional/ +directory.

+ +

+Functional test cases are written very similar to unit test cases. The method such as +open($url) are those found in Selenium. All the methods available in Selenium are available. +

+ +

Run your first unit test case from your browser

+

Point your browser to your development server's unit test case runner, e.g. + http://web-server-address/time-tracker/tests/functional.php. You should see the following + +

Figure 4: Functional test runner
+

+

Clicking on the All button, you should see + +

Figure 5: Functional test success
+

+ +
\ 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 @@ + +

Writing a Unit Test

+

Unit testing is a useful tool when we want to start to test + our individual business logic classes. + The tests/unit directory will be used to hold the unit test cases and tests/functional directory +to hold the function test cases. +

+ +

Write a unit test case

+

We will start be writing a very simple unit test case.

+ +<?php +class ProjectTestCase extends UnitTestCase +{ + function testProjectClassExists() + { + $project = new Project(); + $this->pass(); + } +} +?> + +

Save the code as ProjectTestCase.php in the document_root/time-tracker/tests/unit/ +directory.

+ +

Run your first unit test case from your browser

+

Point your browser to your development server's unit test case runner, e.g. + http://web-server-address/time-tracker/tests/unit.php. You should see the following + +

Figure 1: Unit test runner
+

+

Clicking on the ProjectTestCase.php like, you should see + +

Figure 2: Unit test failure
+

+ +

Smallest step to make the test pass.

+ +

Obviously, we need create the class Project, so lets define the class.

+ +<?php +class Project +{ +} +?> + +

Save the above code as time-tracker/protected/pages/APP_CODE/Project.php. + Where the APP_CODE directory will contain most of your business logic code for the Time Tracker application.

+

We also need to add the following line in our test case so as to include the Project class file when running the tests.

+ +

+The statement Prado::using('Application.APP_CODE.Project') basically +loads the Project.php class file. It assumes that a class name Project has filename Project.php. +For futher details regarding Prado::using can be found in Prado Namespaces documentation. +

+ + +<?php +Prado::using('Application.APP_CODE.Project'); +class ProjectTestCase extends UnitTestCase +{ + ... +} +?> + +

Run the unit test runner again, we see that the test has passed. + +

Figure 3: Unit test success
+

+

+Later on, we shall write more test cases. See the SimpleTest documentation for detailed tutorial on writing test cases.

+ +
\ 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 @@ + + + + + + + + \ 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.png new file mode 100644 index 00000000..f2209ef4 Binary files /dev/null and b/demos/time-tracker/protected/pages/Docs/db.png differ diff --git a/demos/time-tracker/protected/pages/Docs/functional_test1.png b/demos/time-tracker/protected/pages/Docs/functional_test1.png new file mode 100644 index 00000000..33908734 Binary files /dev/null and b/demos/time-tracker/protected/pages/Docs/functional_test1.png differ diff --git a/demos/time-tracker/protected/pages/Docs/functional_test2.png b/demos/time-tracker/protected/pages/Docs/functional_test2.png new file mode 100644 index 00000000..fb507e72 Binary files /dev/null and b/demos/time-tracker/protected/pages/Docs/functional_test2.png differ 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 @@ + +

Prado Time Tracker

+

This documentation is complete walk-through guide detailing the +steps involved in implementating of the Prado Time Tracker web application. +

+

Target Audience

+

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.

+

Questions and Comments

+

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 +http://www.pradosoft.com/forum/. +

+ +
\ 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.png new file mode 100644 index 00000000..a250a943 Binary files /dev/null and b/demos/time-tracker/protected/pages/Docs/project1.png differ diff --git a/demos/time-tracker/protected/pages/Docs/unit_test1.png b/demos/time-tracker/protected/pages/Docs/unit_test1.png new file mode 100644 index 00000000..66b62e19 Binary files /dev/null and b/demos/time-tracker/protected/pages/Docs/unit_test1.png differ diff --git a/demos/time-tracker/protected/pages/Docs/unit_test2.png b/demos/time-tracker/protected/pages/Docs/unit_test2.png new file mode 100644 index 00000000..e33544d2 Binary files /dev/null and b/demos/time-tracker/protected/pages/Docs/unit_test2.png differ diff --git a/demos/time-tracker/protected/pages/Docs/unit_test3.png b/demos/time-tracker/protected/pages/Docs/unit_test3.png new file mode 100644 index 00000000..bbc04551 Binary files /dev/null and b/demos/time-tracker/protected/pages/Docs/unit_test3.png differ -- cgit v1.2.3