48、构建旅行费用应用程序:从测试到实现

构建旅行费用应用程序:从测试到实现

在开发旅行费用应用程序时,我们需要确保各个组件的正确性和可靠性。本文将详细介绍如何通过测试驱动开发(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 的重构使得代码更加简洁和复用。希望本文对你理解如何开发和优化旅行费用应用程序有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值