构建旅行费用应用程序:从测试到实现
在开发旅行费用应用程序时,我们需要确保各个组件的正确性和可靠性。本文将详细介绍如何通过测试驱动开发(TDD)的方法来构建
TravelExpenseWeek
类,并对其进行优化和重构。
1. 旅行费用周测试
在
TravelExpenseItem
持久化之后,我们进行了一系列测试,以验证
TravelExpenseWeek
类的功能。这些测试展示了该类能够根据员工 ID 和起始周检索一周的
TravelExpenseItem
。以下是相关测试代码:
$item8->persist();
$item9->persist();
$week = new TravelExpenseWeek (
array ('emp_id' => "1",
'week_start' => "1980-01-06"),
$this->_session->getDatabaseHandle());
$week->readWeek();
// monday
$this->assertEquals(1.1, (float) $week->getExpenseAmount(0, 'lodging_and_hotel'));
$this->assertEquals(2.2, (float) $week->getExpenseAmount(0, 'meals_breakfast'));
$this->assertEquals(3.3, (float) $week->getExpenseAmount(0, 'misc_supplies'));
// tuesday
$this->assertEquals(4.4, (float) $week->getExpenseAmount(1, 'lodging_and_hotel'));
$this->assertEquals(5.5, (float) $week->getExpenseAmount(1, 'meals_breakfast'));
$this->assertEquals(6.6, (float) $week->getExpenseAmount(1, 'misc_supplies'));
// wednesday
$this->assertEquals(7.7, (float) $week->getExpenseAmount(2, 'lodging_and_hotel'));
$this->assertEquals(8.8, (float) $week->getExpenseAmount(2, 'meals_breakfast'));
$this->assertEquals(9.9, (float) $week->getExpenseAmount(2, 'misc_supplies'));
这些测试表明,
TravelExpenseWeek
类能够按周迭代
TravelExpenseItem
,并且可以使用偏移量来指定目标日期(0 表示周日,1 表示周一,依此类推)。
2. 数据显示与数据结构
为了避免硬编码 HTML 带来的维护噩梦,我们考虑使用以下数据结构来生成需要显示的内容,并将其映射到数据字段:
array(
array('name' => 'Lodging', 'code' => 'lodging', 'data' =>
array('Lodging & Hotel','Other','Tips'),
'persist' => array('lodging_and_hotel', 'lodging_other',
'lodging_tips')),
array('name' => 'Meals', 'code' => 'meals', 'data' =>
array('Breakfast', 'Lunch & Snacks',
'Dinner', 'Tips', 'Entertainment'),
'persist' => array('meals_breakfast', 'meals_lunch',
'meals_dinner', 'meals_tips',
'meals_entertainment')),
array('name' => 'Transportation', 'code' => 'trans', 'data' =>
array('Airfare', 'Auto Rental', 'Auto Maint./Gas',
'Local Transportation', 'Tolls/Parking'),
'persist' => array ('trans_airfare',
'trans_auto_rental', 'trans_auto_maint', 'trans_local',
'trans_tolls', 'trans_miles_traveled')),
array('name' => 'Miscellaneous', 'code' => 'misc', 'data' =>
array('Gifts', 'Telephone & Fax', 'Supplies',
'Postage', 'Other'),
'persist' => array('misc_gifts','misc_phone',
'misc_supplies', 'misc_postage', 'misc_other')));
这个数据结构将 HTML、JavaScript 计算、数据库和 PHP 对象连接在一起,是我们开发的路线图。
3. 更多旅行费用周测试
我们还进行了另外两个重要测试:解析请求测试和持久化容器测试。
解析请求测试
function testTravelExpenseWeekContainerParseRequest() {
$response = array ( 'lodging_sun_0' => "1.1",
'meals_sun_0' => "2.2",
'misc_sun_2' => "3.3",
'lodging_mon_0' => "4.4",
'meals_mon_0' => "5.5",
'misc_mon_2' => "6.6",
'lodging_tue_0' => "7.7",
'meals_tue_0' => "8.8",
'misc_tue_2' => "9.9" );
$week = new TravelExpenseWeek (
array ('emp_id' => "1",
'week_start' => "1980-01-06"));
$week->parse($response);
$this->assertEquals(1.1, $week->getExpenseAmount(0, 'lodging_and_hotel'));
$this->assertEquals(2.2, $week->getExpenseAmount(0, 'meals_breakfast'));
$this->assertEquals(3.3, $week->getExpenseAmount(0, 'misc_supplies'));
$this->assertEquals(4.4, $week->getExpenseAmount(1, 'lodging_and_hotel'));
$this->assertEquals(5.5, $week->getExpenseAmount(1, 'meals_breakfast'));
$this->assertEquals(6.6, $week->getExpenseAmount(1, 'misc_supplies'));
$this->assertEquals(7.7, $week->getExpenseAmount(2, 'lodging_and_hotel'));
$this->assertEquals(8.8, $week->getExpenseAmount(2, 'meals_breakfast'));
$this->assertEquals(9.9, $week->getExpenseAmount(2, 'misc_supplies'));
}
这个测试模拟了从表单输入接收到的响应,并验证了
TravelExpenseWeek
类能够正确解析请求并生成
TravelExpenseItem
。
持久化容器测试
function testTravelExpenseWeekContainerWrite() {
$this->_session->getDatabaseHandle()->query("delete FROM
travel_expense_item WHERE emp_id = 1 and
expense_date >= '1980-01-06' and expense_date
<= ' 2001 - 09 - 15' " );
$response = array (
'lodging_sun_0'=> "1.1",'meals_sun_0'=> "2.2",'misc_sun_2'=> "3.3",
'lodging_mon_0'=> "4.4",'meals_mon_0'=> "5.5",'misc_mon_2'=> "6.6",
'lodging_tue_0'=> "7.7",'meals_tue_0'=> "8.8",'misc_tue_2'=> "9.9" );
$week = new TravelExpenseWeek (
array ('emp_id' => "1",
'week_start' => "1980-01-06",
'territory_worked' => "Midwest",
'comments' => "comment",
'cash_advance' => "0",
'mileage_rate' => "0.31"),
$this->_session->getDatabaseHandle());
$week->parse($response);
$this->assertEquals(true, $week->persist());
$week = new TravelExpenseWeek (
array ('emp_id' => "1",
'week_start' => "1980-01-06",
'territory_worked' => "Midwest",
'comments' => "comment",
'cash_advance' => "0",
'mileage_rate' => "0.31"),
$this->_session->getDatabaseHandle());
$week->readWeek();
$this->assertEquals(1.1, (float)
$week->getExpenseAmount(0, 'lodging_and_hotel'));
$this->assertEquals(2.2, (float)
$week->getExpenseAmount(0, 'meals_breakfast'));
$this->assertEquals(3.3, (float)
$week->getExpenseAmount(0, 'misc_supplies'));
$this->assertEquals(4.4, (float)
$week->getExpenseAmount(1, 'lodging_and_hotel'));
$this->assertEquals(5.5, (float)
$week->getExpenseAmount(1, 'meals_breakfast'));
$this->assertEquals(6.6, (float)
$week->getExpenseAmount(1, 'misc_supplies'));
$this->assertEquals(7.7, (float)
$week->getExpenseAmount(2, 'lodging_and_hotel'));
$this->assertEquals(8.8, (float)
$week->getExpenseAmount(2, 'meals_breakfast'));
$this->assertEquals(9.9, (float)
$week->getExpenseAmount(2, 'misc_supplies'));
}
这个测试验证了
TravelExpenseWeek
类能够将
TravelExpenseItem
持久化到数据库,并从数据库中重新读取和验证数据。
4. 满足旅行费用周测试
为了满足
TravelExpenseWeek
的有效性和持久性测试,我们实现了以下类:
class TravelExpenseWeek extends PersistableObject {
public $items = array();
function __construct ($results, $dbh = null) {
parent::__construct ($results, $dbh);
}
public function isValid() {
if ($this->isEmpty("emp_id") == true) return false;
if ($this->isEmpty("week_start") == true) return false;
if ($this->isEmpty("territory_worked") == true) return false;
if ($this->isEmpty("mileage_rate") == true) return false;
return true;
}
public function persist() {
return $this->persistWork ("travel_expense_week",
array ( "emp_id",
"week_start",
"comments",
"territory_worked",
"cash_advance",
"mileage_rate"));
}
public function getSqlWhere() {
return " emp_id = ".$this->emp_id." and week_start
= '".$this->week_start."'";
}
public function parse( & $request) { }
public function readWeek() { }
public function getExpenseAmount($offset, $description) { }
}
其中,
parse
、
readWeek
和
getExpenseAmount
函数是测试所需的语法存根,后续需要实现具体功能。
5. 满足解析请求测试
为了满足解析请求测试,我们需要实现以下功能:
获取元数组
public function getExpensesMetaArray () {
return array(
array('name' => 'Lodging', 'code' => 'lodging', 'data' =>
array('Lodging & Hotel','Other','Tips'), 'persist' =>
array('lodging_and_hotel', 'lodging_laundry',
'lodging_tips')),
array('name' => 'Meals', 'code' => 'meals', 'data' =>
array('Breakfast', 'Lunch & Snacks',
'Dinner', 'Tips', 'Entertainment'), 'persist' =>
array('meals_breakfast', 'meals_lunch',
'meals_dinner', 'meals_tips',
'meals_entertainment')),
array('name' => 'Transportation', 'code' => 'trans', 'data' =>
array('Airfare', 'Auto Rental', 'Auto Maint./Gas',
'Local Transportation',
'Tolls/Parking'), 'persist' =>
array ('trans_airfare', 'trans_auto_rental',
'trans_auto_maint', 'trans_local',
'trans_tolls', 'trans_miles_traveled')),
array('name' => 'Miscellaneous', 'code' => 'misc', 'data' =>
array('Gifts', 'Telephone & Fax', 'Supplies',
'Postage', 'Other'), 'persist' =>
array('misc_gifts','misc_phone',
'misc_supplies', 'misc_postage',
'misc_other')));
}
获取费用金额
/**
* todo: put into an associative array
*/
public function getExpenseAmount($offset, $description) {
$targetDate = $this->addDays($this->week_start, $offset);
foreach ($this->items as $item) {
if ($item->expense_date == $targetDate &&
$item->description == $description) {
return $item->amount;
}
}
return "";
}
添加天数
/**
* todo: will this fail on daylight savings time?
*/
public function addDays($start, $days) {
return date("Y-m-d", strtotime($start)+$days*86400);
}
解析请求
/**
* This function bridges the gap between the day-based DB and the
* week-based view
*/
public function getWeekArray() {
return array ('sun', 'mon', 'tue', 'wed', 'thr', 'fri', 'sat');
}
public function parse ( & $request) {
// section loop
foreach ($this->getExpensesMetaArray() as $sectionlist) {
// row loop
for ($i=0; $i < count ($sectionlist['persist']); $i++) {
$daynum = 0;
// day loop
foreach ($this->getWeekArray() as $day) {
$index = $sectionlist['code']."_".$day."_".$i;
if (array_key_exists($index, $request) and
$request[$index] <> null and
$request[$index] <> " " ) {
// create new item and store in $this->items
array_push (
$this->items,
new TravelExpenseItem (
array ('emp_id' => $this->emp_id,
'expense_date' =>
$this->addDays($this->week_start,
$daynum),
'description' =>
$sectionlist['persist'][$i],
'amount' => (float) $request[$index]),
$this->dbh));
}
$daynum++;
}
}
}
}
这些函数构成了
TravelExpenseWeek
类的主要功能,它负责将基于 Web 的数据结构与以数据库为中心的数据结构进行转换。
6. 满足旅行费用周容器读写测试
为了满足读写测试,我们实现了
readWeek
函数:
public function readWeek() {
$sql = "select * from travel_expense_week where";
$sql .= " emp_id = ".$this->emp_id." and ";
$sql .= " week_start = '".$this->week_start."'";
$result = $this->dbh->query($sql);
if (DB::isError($result) <> true and $result->numRows() >
0) {
$row = $result->fetchRow();
$this->contentBase[‘comments’] = $row[‘comments’];
$this->contentBase[‘territory_worked’] = $row[‘territory_worked’];
$this->contentBase[‘cash_advance’] = $row[‘cash_advance’];
$this->contentBase[‘mileage_rate’] = $row[‘mileage_rate’];
}
$sql = “select * from travel_expense_item where”;
$sql .= ” emp_id = “.$this->emp_id.” and “;
$sql .= ” expense_date >= ‘“.$this->week_start.”’ and”;
$sql .= ” expense_date <= ‘“.$this->addDays($this->week_start, 6).”’“;
$this->items = array();
$result = $this->dbh->query($sql);
if (DB::isError($result) or $result->numRows() == 0) return;
while ($row = $result->fetchRow()) {
array_push ($this->items, new TravelExpenseItem ($row));
}
}
这个函数分两部分进行数据库查询,第一部分查询 `TravelExpenseWeek` 的状态,第二部分查询关联的 `TravelExpenseItem` 并添加到 `TravelExpenseWeek->items` 数组中。
然而,这里存在一个潜在问题,在将整个 `$row` 传递给 `TravelExpenseItem` 构造函数时,可能会盲目存储不必要的数据,造成资源浪费。为了解决这个问题,我们创建了一个新测试:
```php
function testIgnoreExtra() {
$response = array ('emp_id' => "1",
'expense_date' => "1980-01-01",
'description' => "one",
' amount ' => " 1.0 ",
'extra' => "extra bits");
$tvi = new TravelExpenseItem($response);
$this->assertEquals(null, $tvi->extra);
}
这个测试失败了,说明
TravelExpenseItem
会存储额外的数据。为了改变
PersistableObject
的默认行为,我们对其进行了重写:
class TravelExpenseItem extends PersistableObject {
protected $contentMetaTable = null;
protected $contentMetaOnly = null;
function __construct ($results, $dbh = null) {
$this->contentMetaTable = "travel_expense_item";
$this->contentMetaOnly = array ( "emp_id",
"expense_date",
"description",
"amount");
$content = array();
foreach ($this->contentMetaOnly as $key) {
if (array_key_exists($key, $results))
$content[$key] = $results[$key];
}
parent::__construct ($content, $dbh);
}
public function isValid() {
if ($this->isEmpty("emp_id") == true) return false;
if ($this->isEmpty("expense_date") == true) return false;
if ($this->isEmpty("description") == true) return false;
if ($this->isEmpty("amount") == true) return false;
return true;
}
public function getSqlWhere() {
return " emp_id = ".$this->emp_id." and expense_date
='".$this->expense_date."' and description = '".$this->description."'";
}
public function persist() {
return $this->persistWork (
$this->contentMetaTable,
$this->contentMetaOnly);
}
}
这样,
TravelExpenseItem
只会存储期望的数据。
TravelExpenseWeek
的
persist
函数也需要修改,在保存
TravelExpenseWeek
本身后,还需要保存其关联的
TravelExpenseItem
:
public function persist() {
$this->persistWork ("travel_expense_week",
array("emp_id",
"week_start",
"comments",
"territory_worked",
"cash_advance",
" mileage_rate " ));
// persist each item to the database
foreach ($this->items as $item) {
$item->persist();
}
return true;
}
7. 快速重构
为了提高代码的可维护性和复用性,我们对
PersistableObject
进行了重构,将
TravelExpenseItem
和
TravelExpenseWeek
中相似的功能提取到
PersistableObject
中:
class PersistableObject {
protected $contentBase = array();
protected $contentMetaTable = null;
protected $contentMetaOnly = null;
protected $dbh = null; // database handle
protected $dispatchFunctions = array ("role" => "getrole");
function __get ($key) {
// content removed for brevity
}
function __construct ($results, $dbh = null) {
$this->dbh = $dbh;
if ($this->contentMetaOnly <> null) {
foreach ($this->contentMetaOnly as $key) {
if (array_key_exists($key, $results)) {
$this->contentBase[$key] = $results[$key];
}
}
} elseif ($results <> null) {
$this->contentBase = $results; // copy
}
}
public function implodeQuoted ( & $values, $delimiter) {
// content removed for brevity
}
public function generateSqlInsert ($tableName, $metas, $values) {
// content removed for brevity
}
public function generateSqlInsert ($tableName, $metas, $values) {
// content removed for brevity
}
public function generateSqlUpdate ($tableName, $metas, $values) {
// content removed for brevity
}
public function generateSqlDelete ($tableName) {
// content removed for brevity
}
public function getSqlWhere() {
// content removed for brevity
}
protected function isEmpty($key) {
// content removed for brevity
}
public function isValid() {
// content removed for brevity
}
public function persistWork ($tablename, $meta) {
// content removed for brevity
}
public function persist() {
return $this->persistWork (
$this->contentMetaTable,
$this->contentMetaOnly);
}
}
重构后,
TravelExpenseItem
和
TravelExpenseWeek
的代码变得更加简洁:
class TravelExpenseItem extends PersistableObject {
function __construct ($results, $dbh = null) {
$this->contentMetaTable = "travel_expense_item";
$this->contentMetaOnly = array ( "emp_id",
"expense_date",
"description",
"amount");
parent::__construct ($results, $dbh);
}
public function isValid() {
if ($this->isEmpty("emp_id") == true) return false;
if ($this->isEmpty("expense_date") == true) return false;
if ($this->isEmpty("description") == true) return false;
if ($this->isEmpty("amount") == true) return false;
return true;
}
public function getSqlWhere() {
return " emp_id = ".$this->emp_id." and expense_date =
'".$this->expense_date."' and description = '".$this->description."'";
}
}
class TravelExpenseWeek extends PersistableObject {
public $items = array();
function __construct ($results, $dbh = null) {
$this->contentMetaTable = "travel_expense_week";
$this->contentMetaOnly = array ( "emp_id",
"week_start",
"comments",
"territory_worked",
"cash_advance",
"mileage_rate");
parent::__construct ($results, $dbh);
}
public function isValid() {
// content removed for brevity
}
public function persist() {
if (parent::persist() == false) return false;
// persist each item to the database
foreach ($this->items as $item) {
if ($item->persist() == false) return false;
}
return true;
}
public function getSqlWhere() {
return " emp_id = ".$this->emp_id." and week_start =
'".$this->week_start."'";
}
public function getExpensesMetaArray () {
// content removed for brevity
}
public function getWeekArray() {
// content removed for brevity
}
public function addDays($start, $days) {
// content removed for brevity
}
public function parse ( & $request) {
// content removed for brevity
}
public function readWeek() {
// content removed for brevity
}
public function getExpenseAmount($offset, $description) {
// content removed for brevity
}
}
8. 修复测试问题
在测试过程中,我们发现
TravelExpenseWeek
的
cash_advance
和
comments
字段存在问题。原有的有效性测试要求
cash_advance
必须存在,但实际上该字段不是必需的。我们修改了测试用例:
function testTravelExpenseWeekPersistence() {
$this->_session->getDatabaseHandle()->query("delete FROM
travel_expense_week WHERE emp_id = 1 and
week_start = '1980-01-01'");
$tvi = new TravelExpenseWeek (
array ('emp_id' => "1",
'week_start' => "1980-01-01",
'territory_worked' => "Midwest",
'mileage_rate' => "0.31"),
$this->_session->getDatabaseHandle());
$result = $this->_session->getDatabaseHandle()->query("select *
FROM travel_expense_week WHERE emp_id = 1 and week_start
= '1980-01-01'");
$this->assertEquals(0, $result->numRows(), "pre check");
$this->assertEquals(true, $tvi->persist(), "save");
$result = $this->_session->getDatabaseHandle()->query("select *
FROM travel_expense_week WHERE emp_id = 1 and week_start
= '1980-01-01'");
$this->assertEquals(1, $result->numRows(), "persisted ok");
$row = $result->fetchRow();
$this->assertEquals(0.0, (float) $row['cash_advance'],
"cash advance default");
}
同时,我们修改了
TravelExpenseWeek
的构造函数,设置
cash_advance
和
comments
的默认值:
class TravelExpenseWeek extends PersistableObject {
function __construct ($results, $dbh = null) {
$this->contentMetaTable = "travel_expense_week";
$this->contentMetaOnly = array ( "emp_id",
"week_start",
"comments",
"territory_worked",
"cash_advance",
"mileage_rate");
$this->contentBase['comments'] = "";
$this->contentBase['cash_advance'] = "0.0";
parent::__construct ($results, $dbh);
}
}
总结
通过一系列的测试和重构,我们成功实现了
TravelExpenseWeek
类的功能,并解决了潜在的问题。整个开发过程遵循测试驱动开发的原则,确保了代码的正确性和可维护性。以下是开发过程的流程图:
graph TD;
A[编写测试用例] --> B[实现功能代码];
B --> C[运行测试];
C --> D{测试是否通过};
D -- 否 --> B;
D -- 是 --> E[重构代码];
E --> F[再次运行测试];
F --> G{测试是否通过};
G -- 否 --> E;
G -- 是 --> H[完成开发];
在开发过程中,我们还使用了数据结构来避免硬编码 HTML,提高了代码的灵活性和可维护性。同时,对
PersistableObject
的重构使得代码更加简洁和复用。希望本文对你理解如何开发和优化旅行费用应用程序有所帮助。
超级会员免费看
1927

被折叠的 条评论
为什么被折叠?



