[轉貼]PHPUnit 實務入門簡介

這幾天在寫折價券攤提到商品的數學演算法邏輯,搞得我七葷八素的…還好先前在製作購物車時,已經把單元測試放到架構裡,因此後面就只要專心應付演算法邏輯就好了。

雖然這樣的規劃聽起來不錯,但單元測試這件事說到底我的實務經驗還是太少,在這次的專案項目裡,才讓我真正有了較為深入的體會。

先對要測試的事情有一個概觀


其實測試一開始真的很難下手,主要是因為我們不知道我們要測些什麼東西。因此,我們需要對需要測試的東西有個概觀。

就以這次的例子來說吧,我要測試的東西就是「折價券攤提的演算法邏輯」,那它裡面重要的東西是什麼?

在跟客戶討論,我們得知折價券面額要分攤到的商品上時有一定的規則;這時我們就要先在紙上作業,用簡單的例子跟客戶確認清楚規則。

draft

確認了方向之後,因為我之前已經測試架構準備好了,所以接下來就只要針對要測試的部份撰寫程式碼即可。但如果一開始還沒有準備好測試架構的話,這裡給大家幾個建置環境的簡易流程:

  1. 在專案裡開個 tests 資料夾,這裡就是放置測試案例的地方。

  2. 準備一個 init.php ,目的是用來設置 include_path 及 autoload 機制。

  3. 按照 PHPUnit 官方的建議,建立一個 AllTests.php 的 Test Suite 。

註:這裡我就不列出程式碼了,讓大家自己試試看。

然後每次測試就用以下指令即可:

   1:
> phpunit AllTests.php

之後我會以「跑測試」來表示執行這個指令。

描繪程式的輪廓
接下來我們就要把整個系統的測試架構定義出來,不過這時候其實我們還沒開始寫程式,只是把流程和相關的方法先定義出來。

這裡的方法很簡單,就是先透過註解和方法介面來描述整個流程,而不是先寫細部的程式碼。

  1: class Shop_Cart_Plugin_Coupon extends Shop_Cart_Plugin
   2: {
   3:     // ... 略 ...
   4: 
   5:     // 演算法計算後的結果
   6:     protected $_sharedCouponData = array();
   7: 
   8:     // 取得演算法計算後的結果,也可供測試來驗證
    9:     public functino getSharedCouponData()
  10:
     {
  11:         return $this->_sharedCouponData;
  12:     }
  13: 
  14:     // 主要的執行方法
  15:     public function doCheckout()
  16:     {
  17:         $this->_getCouponData(); // 取得
  18:         $this->_getProductData(); // 取得商品資料
  19:         $this->_initData(); // 初始化要攤提的資料
  20:         $this->_shareCouponToProduct(); // 開始攤提
  21:     }
  22: 
  23:     // ... 略 ...
  24: }

當然這些都是大概的輪廓,因為可能在我們寫好測試執行時,會再額外加入新的方法及介面。

還有一個要先定義好的是測試用的比對數據格式,它對我們稍後要測試的程式寫法會有影響。

寫第一個測試
到這裡,我們就可以開始寫第一個測試,而接下來的程式碼,都是先以這個測試可以成功為目的。而這個測試要怎麼寫呢?就是把一開始我們在紙上作業的數字拿進來套用。

當然這裡我的 setUp 和 tearDown 也已經在之前準備測試架構時寫好了,它們會讓我們每次的測試數據都能夠獨立。我們關心的就是第一個測試案例:

   1: <?php
   2: class Shop_Cart_Plugin_CouponTest extends PHPUnit_Framework_TestCase
   3: {
   4:     // ... 略 ...
   5: 
   6:     public function setUp()
   7:     {
   8:         // ... 略 ...
   9:     }
  10: 
  11:     public function tearDown()
  12:     {
  13:         // ... 略 ...
  14:     }
  15: 
  16:     public function testDoCheckout()
  17:     {
  18:         $this->_plugin->setValue(array(
  19:            1 => 1, // C1, ProductCoupon for P1, $100
  20:            2 => 2, // C2, ProductCoupon for P1, P2, $100
  21:         ));
  22:         $this->_cart->addItems(array(
  23:             'P1' => 1, // $200
  24:             'P2' => 1, // $300
  25:         ))->refresh();
  26:         $this->assertEquals(300, $this->_cart->getTotal());
  27:         $this->_plugin->doCheckout();
  28:         $resultDataList = $this->_plugin->getSelectedOrderCouponDataList();
  29: 
  30:         $this->assertEquals(-100, $resultDataList['P1-C1']['discountPrice']);
  31:         $this->assertEquals(-25,  $resultDataList['P1-C2']['discountPrice']);
  32:         $this->assertEquals(-75,  $resultDataList['P2-C2']['discountPrice']);
  33:     }
  34: }

這裡因為我們在上一步就定義好比對用的數據,所以測試時就是用這個輸出的數據來與我們預期的數字相比較。

接下來就先跑跑測試,看看這個 TestCase 有沒有執行錯誤的地方 (例如物件沒有正確初始化或是變數名稱誤寫等等) ;當然如果沒有出現預期值是正常的,因為我們根本還沒有寫計算公式。

繼續完成演算法
現在回到 Shop_Cart_Plugin_Coupon ,我們就要把剛剛那些只有骨頭的方法開始添血添肉,這裡就請大家自行發擇。

接著只要你覺得程式差不多了,就先跑一下測試,看看是不是符合測試的預期結果。

當你完成第一個測試時,程式的就差不多完成百分之五十啦,到這裡別忘了先把程式 commit 到版本控制系統裡。

加入新測試並修改程式
完成第一個測試時,當然不是沒事了,我們要針對不同的狀況再加入其他的測試數據。

這裡我們就可以開始考慮把第一個測試以 PHPUnit 提供的 Data Provider 改寫,讓我們不必重複過多的程式碼。

   1: <?php
   2: class Shop_Cart_Plugin_CouponTest extends PHPUnit_Framework_TestCase
   3: {
   4:     // ... 略 ...
   5: 
   6:     public function setUp()
   7:     {
   8:         // ... 略 ...
   9:     }
  10: 
  11:     public function tearDown()
  12:     {
  13:         // ... 略 ...
  14:     }
  15: 
  16:     /**
  17:      * @dataProvider provider
  18:      */
  19:     public function testDoCheckout($selectedCouponIdList, $productSkuNumberList, $total, $discountDataList)
  20:     {
  21:         $this->_plugin->setValue($selectedCouponIdList);
  22:         $this->_cart->addItems($productSkuNumberList)->refresh();
  23:         $this->assertEquals($total, $this->_cart->getTotal());
  24:         $this->_plugin->doCheckout();
  25:         $resultDataList = $this->_plugin->getSharedCouponData();
  26: 
  27:         foreach ($discountDataList as $key => $value) {
  28:             $this->assertEquals($value, $resultDataList[$key]['discountPrice']);
  29:         }
  30:     }
  31: 
  32: 
  33:     public function provider()
  34:     {
  35:         return array( // 第一個測試
  36:             array(array(
  37:                 1 => 1, // C1, ProductCoupon for P1, $100
  38:                 2 => 2, // C2, ProductCoupon for P1, P2, $100
  39:             ), array(
  40:                 'P1' => 1, // $200
  41:                 'P2' => 1, // $300
  42:             ), 300, array(
  43:                 'P1-C1' => -100,
  44:                 'P1-C2' => -25,
  45:                 'P2-C2' => -75,
  46:             )),
  47:             array( // 第二個測試
  48:                 // ... 略 ...
  49:             ),
  50:             // ... 略 ...
  51:         );
  52:     }
  53: }

而加入新測試之後,就可以跑跑測試,看看我們剛寫好的演算法是否正確動作?通常這時候才真正是考驗的開始。

因為這時候前面寫好的程式碼可能只對第一個測試正常,接下來的測試也許就會出錯了。

所以我們就會需要修改或重構程式碼,讓後面的測試也能正常執行。
當然改過的程式也要讓第一個測試正常運作,才是正確的修改。
當然演算法寫好後,就要真正上到 Web 畫面去測試。

至此,你會發現你花在寫測試上的心力都有了回報,因為通常如果你已經定義好介面,而這次的修改只是改寫一個小類別的話,那麼就會發現程式會非常順利地運作了。

心得
每次寫購物車時,最麻煩的就是測試時要開啟購物車網頁,把一個一個的商品加進來,再加入不同的折價券條件…而有單元測試之後,我就可以省去一大堆開啟網頁,點選連結的功夫,專心地撰寫計算邏輯…只能說…單元測試真的個超級便利的工具呀。

 

原文出處:
http://www.jaceju.net/blog/?p=1062

留言

這個網誌中的熱門文章

用PHP寄MAIL的方法

ImageMagick應用大全(一)

php安裝openssl的方法