開發網站時,寫測試的3個簡單方法

許多想要學寫測試的朋友,常常不知道怎麼開始。

其實寫測試比大多數人以為的還要簡單。

就算是小型的網站、簡單的購物網站,也可以利用軟體測試加強軟體品質。

今天跟大家分享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.)