summaryrefslogtreecommitdiff
path: root/demos/time-tracker/protected/pages/Docs/UserClassAndExceptions.page
blob: f85e00be3bbf36a7dcbf62f428aa75dccf805979 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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">
&lt;?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');
	}
}
?&gt;
</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">
&lt;?php
class TimeTrackerException extends TException
{
	/**
	 * @return string path to the error message file
	 */
	protected function getErrorMessageFile()
	{
        return dirname(__FILE__).'/exceptions.txt';
	}	
}
?&gt;
</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>