許多想要學寫測試的朋友,常常不知道怎麼開始。
其實寫測試比大多數人以為的還要簡單。
就算是小型的網站、簡單的購物網站,也可以利用軟體測試加強軟體品質。
今天跟大家分享3個簡單的導入測試開發方法。
(本文使用 Laravel + PHPUnit 示範,但相關概念通用在任何語言/工具上。)
第一招、controller轉移到entity
開發網站時,大部份商業邏輯都在對entity(例:產品、訂單、使用者…etc)操作。
相關的code通常會散在controller裡面。
舉例來說,假設我們在寫一個購物網站,那麼把產品加進訂單、更新訂單金額的code會像這樣:
// ShoppingController.php $product = Product::find(Input::get('product_id')); $order->subtotal += $product->price
寫測試的目的是為了增加開發者的信心。
像上面這樣的code,有些人會覺得打開瀏覽器按個幾次,看看最後訂單金額是否正確就好。
反覆按個幾次,就算不寫測試,也對程式品質很有把握、相信它沒有bug。
但如果程式再複雜一點呢?例如增加不同運費的邏輯:
// ShoppingController.php // 滿千元免運費 if ($order->subtotal > 1000) { $order->shipping_fee = 0; } else { $order->shipping_fee = 100; }
有些人依然覺得這不複雜,在瀏覽器上多操作幾次,然後看看金額、運費是否都正確即可。
但如果你像我一樣,採取比較嚴謹的立場,預設自己寫的所有code都有bug,那麼可以先把code從controller搬到entity:
// Order.php class Order extends Eloquent { public function addProdut($product) { $this->subtotal += $product->price if ($this->subtotal > 1000) { $this->shipping_fee = 0; } else { $this->shipping_fee = 100; } } }
然後可以這樣寫測試:
//OrderTest.php class OrderTest extends TestCase { public function test_addProdut_with_shipping_fee() { $product = new Product(); $product->price = 800; $order = new Order(); $order->addProduct($product); $this->assertEquals(100, $order->shipping_fee); } public function test_addProdut_without_shipping_fee() { $product = new Product(); $product->price = 1500; $order = new Order(); $order->addProduct($product); $this->assertEquals(0, $order->shipping_fee); } }
像這樣的測試寫好之後,就不用每次改動一點商業邏輯,就打開瀏覽器重複做一堆人工測試了。
如果運費的算法更複雜,根據訂單金額而有4、5種運費金額的話,
光是要測試運費邏輯,就要人工去操作下單流程好幾次。
這種時候,寫測試方便多了。
這種開發流程,我稱為「controller轉移到entity」。
一開始邏輯單純的時候,直接寫在controller即可。
複雜起來後,再把code轉移進entity,然後補寫測試即可。
第二招、轉移到service
接續上面的例子,來看看更複雜的例子。
現在產品賣掉的時候,產品本身的庫存數量要減一。
以原本的寫法來說,需要在entity加上這一行:
// Order.php $product->amount -= 1;
然後在測試內多加幾行:
//OrderTest.php $originalAmount = $product->amount; $order->addProduct($product); $this->assertEquals($originalAmount - 1, $product->amount);
看起來沒多大問題。邏輯放在Order內還算合理,該測的也都測到了。
但如果邏輯再複雜一點呢?
假設有人氣指數的功能,在賣出去的同時,商品的人氣指數要加上10分:
$product->trending_score += 10
這行code還是放在Order類別內嗎?
為什麼一定是$order->addProduct($product),而不能是$product->addToOrder($order)呢?
隨著Product相關邏輯變多,商品加入訂單的功能不再完全像是訂單的事,也很像是Product的事。
像這種不知道要放在Order內還是Product內的時候,可以獨立出來成Service類別:
class AddProductToOrder { public function handle($product, $order) { //blah } }
測試的時候,改寫成這樣即可:
class AddProductToOrderTest extends TestCase { public function test_handle_with_shipping_fee() { $product = new Product(); $product->price = 800; $order = new Order(); $service = new AddProductToOrder(); $service->handle($product, $order); $this->assertEquals(100, $order->shipping_fee); } public function test_handle_product_amount() { // blah } public function test_handle_product_trending_score() { // blah } }
跟第一招一樣,可以在程式變大之後、有必要的時候才獨立成service類別並補寫測試。
轉移的成本是很低的,幾乎只是把code從這邊剪下貼上到那邊而已。
第三招、轉移到專門的POPO
POPO(Plain Old PHP Object)是指單純的全手寫類別,不去繼承Eloquent之類的華麗類別。
雖然是最基本的OOP用法,卻常常被人們所忽略。
現在假設訂單進一步變複雜,運費的邏輯會根據消費者的所在地區而有不同:
class AddProductToOrder { public function handle($product, $order, $country) { // blah if ($country == 'taiwan') { if ($order->subtotal > 1000) { // set shipping fee } else { // set shipping fee } } else if ($country == 'korea') { // set shipping fee } //blah blah } }
再加上金額的變化,光是運費本身的計算就很複雜、需要寫很多測試才安心。
與其在entity或service內計算,不如弄一台運費計算機出來,會更分工明確:
class ShippingFeeCalculator { public function calculate($productPrice, $customerCountry) { // blah } }
接著就能替計算機寫許多測試,測到安心為止:
class ShippingFeeCalculatorTest extends TestCase { public function test_calculate_taiwan_100_dollar_order() { $calculator = new ShippingFeeCalculator(); $result = $calculator->calculate(100, 'taiwan'); $this->assertEquals($someNumber, $result); } public function test_calculate_taiwan_600_dollar_order() { // blah } public function test_calculate_korea_100_dollar_order() { // blah } }
本來的service就可以改寫成,吃計算機當作參數:
class AddProductToOrder { public function handle($product, $order, $country, $calculator) { $shippingFee = $calculator->calculate($product->price, $country); // blah } }
POPO常常被人們忘記,因為太習慣把code全都寫進entity或是controller裡面。
其實,在程式複雜起來之後,最好把意義上獨立的部份各自獨立成類別。
然後分別替這些POPO寫各式各樣的測試,就能大幅增加軟體的品質與穩定性了。
結論
寫測試的目的純粹是為了增加安全感,讓工程師晚上安心入睡而已。
不需要撲天蓋地般地狂寫測試,適量的測試就非常足夠。
以中大型專案來說,寫測試可以幫助省下「非常大量的時間」。
實務上,在一開始商業邏輯不多的時候,寫在controller通常沒什麼問題,把幾個變數存進資料庫而已,不太需要寫測試。
等到邏輯慢慢變複雜,發現不寫測試會不敢上線的時候,再拉出來並且補寫測試即可。
希望你之後的專案開發,可以試試看寫測試帶來的好處與美妙!
歡迎訂閱轉個彎日誌的粉絲專頁,我很樂意和你分享各種心得。
(Photo via Elizabeth Hahn, CC licensed.)