這幾天在寫折價券攤提到商品的數學演算法邏輯,搞得我七葷八素的…還好先前在製作購物車時,已經把單元測試放到架構裡,因此後面就只要專心應付演算法邏輯就好了。
雖然這樣的規劃聽起來不錯,但單元測試這件事說到底我的實務經驗還是太少,在這次的專案項目裡,才讓我真正有了較為深入的體會。
先對要測試的事情有一個概觀
其實測試一開始真的很難下手,主要是因為我們不知道我們要測些什麼東西。因此,我們需要對需要測試的東西有個概觀。 就以這次的例子來說吧,我要測試的東西就是「折價券攤提的演算法邏輯」,那它裡面重要的東西是什麼?
在跟客戶討論,我們得知折價券面額要分攤到的商品上時有一定的規則;這時我們就要先在紙上作業,用簡單的例子跟客戶確認清楚規則。
確認了方向之後,因為我之前已經測試架構準備好了,所以接下來就只要針對要測試的部份撰寫程式碼即可。但如果一開始還沒有準備好測試架構的話,這裡給大家幾個建置環境的簡易流程:
-
在專案裡開個 tests 資料夾,這裡就是放置測試案例的地方。
-
準備一個 init.php ,目的是用來設置 include_path 及 autoload 機制。
-
按照 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