實踐 Laravel phpunit

lxzoliver發表於2020-07-11

使用laravel進行開發已經有一段時間了,可是一直沒有用到laravel的測試。看文件也有點懵懵的,所以現在通過這篇文章,認真記錄相關的測試過程。

文件上說PHPUnit 是一個輕量級的 PHP 測試框架,Laravel 預設就支援用 PHPUnit 來做測試,併為你的應用程式配置好了 phpunit.xml 檔案,只需在命令列上執行 phpunit 就可以進行測試。

當我直接執行phpunit命令時,出現了以下問題:
認真實踐學習 Laravel phpunit

查詢原因,是因為我沒有將phpunit新增到環境變數當中,所以無法直接執行phpunit命令。

由於我使用的系統為mac,所以通過 vim .bash_profile 命令,在檔案最後新增 export PATH="~/你的檔案目錄/vendor/bin:vendor/bin:$PATH",如果不知道檔案目錄,可以通過 pwd命令檢視一下。儲存編輯後,執行source .bash_profile 重新載入一下配置

再執行 phpunit 命令,可檢視到已正常執行了該命令了

實踐 Laravel phpunit

建立測試類

檢視文件:可以使用 Artisan 命令 make:test 建立一個測試用例:

// 在 Feature 目錄下建立一個測試類...
php artisan make:test UserTest

// 在 Unit 目錄下建立一個測試類...
php artisan make:test UserTest --unit

該命令會在 tests/Feature 目錄中建立 UserTest.php 檔案,我們會發現 tests 目錄中有 FeatureUnit 兩個目錄,如何區分這兩個目錄呢?

  • Unit —— 單元測試是從程式設計師的角度編寫的。它們用於確保類的特定方法執行一組特定任務。
  • Feature —— 功能測試是從使用者的角度編寫的。它們確保系統按照使用者期望的那樣執行,包括幾個物件的相互作用,甚至是一個完整的 HTTP 請求。

以下內容大部分取自PHPUnit手冊內容,我則根據相關內容編寫一些測試例子

編寫 PHPUnit 測試

  1. 針對類 Class 的測試寫在類 ClassTest中。

  2. ClassTest(通常)繼承自 PHPUnit\Framework\TestCase

  3. 測試都是命名為 test* 的公用方法。

    也可以在方法的文件註釋塊(docblock)中使用 @test 標註將其標記為測試方法。

  4. 在測試方法內,類似於 assertEquals()(參見 附錄 A)這樣的斷言方法用來對實際值與預期值的匹配做出斷言。

首先通過laravel命令建立一個單元測試類 php artisan make:test CalcTest --unit

在類中新增方法,該測試類主要驗證簡單的加減乘除功能,按照我的理解,主要是在編寫測試方法中,通過新增斷言的方式,執行測試用例是否執行符合我們的預期

class CalcTest extends TestCase
{
    /**
     * @test
     */
    public function add()
    {
        $a = 1;
        $b = 1;
        $this->assertEquals(2,($a+$b));
    }
}

執行phpunit --filter=CalcTest指定所需要執行的測試的檔案類,當然也可以指定具體的測試方法phpunit --filter=add,如果直接執行phpunit,會執行所有的測試用例,但我這裡只想執行CalcTest類下的方法,所以執行phpunit --filter=CalcTest

執行命令顯示如下內容,表示我們所新增的斷言通過

實踐 Laravel phpunit

測試的依賴關係

PHPUnit支援對測試方法之間的顯式依賴關係進行宣告。這種依賴關係並不是定義在測試方法的執行順序中,而是允許生產者(producer)返回一個測試基境(fixture)的例項,並將此例項傳遞給依賴於它的消費者(consumer)們。

  • 生產者(producer),是能生成被測單元並將其作為返回值的測試方法。

  • 消費者(consumer),是依賴於一個或多個生產者及其返回值的測試方法。

class CalcTest extends TestCase
{
    /**
     * @test
     */
    public function add()
    {
        $a = 1;
        $b = 1;
        $this->assertEquals(2,($a+$b));
        return $a+$b;
    }

    /**
     * @test
     * @depends add
     */
    public function multiply($value)
    {
        $this->assertEquals(4,$value*2);
    }
}

新增兩個測試方法,其中multiply方法依賴於add方法,add可進行斷言測試,並將返回值傳遞給multiply方法進行後續斷言測試

執行phpunit --filter=CalcTest命令,結果如下所示

實踐 Laravel phpunit

若我修改程式碼,add方法的斷言測試結果為錯誤的,並執行phpunit --filter=CalcTest命令,結果會是怎樣呢?

class CalcTest extends TestCase
{
    /**
     * @test
     */
    public function add()
    {
        $a = 1;
        $b = 1;
        $this->assertEquals(1,($a+$b));
        return $a+$b;
    }

    /**
     * @test
     * @depends add
     */
    public function multiply($value)
    {
        $this->assertEquals(4,$value*2);
    }
}

執行結果如下所示,在官方文件給出:為了快速定位缺陷,我們希望把注意力集中於相關的失敗測試上。這就是為什麼當某個測試所依賴的測試失敗時,PHPUnit 會跳過這個測試。通過利用測試之間的依賴關係,缺陷定位得到了改進
所以multiply方法的斷言測試在程式執行過程中,是跳過的。

實踐 Laravel phpunit

資料供給器

測試方法可以接受任意引數。這些引數由資料供給器方法(在 例 2.5中,是 additionProvider() 方法)提供。用 @dataProvider 標註來指定使用哪個資料供給器方法。

資料供給器方法必須宣告為 public,其返回值要麼是一個陣列,其每個元素也是陣列;要麼是一個實現了 Iterator 介面的物件,在對它進行迭代時每步產生一個陣列。每個陣列都是測試資料集的一部分,將以它的內容作為引數來呼叫測試方法。

    /**
     * @test
     * @dataProvider additionProvider
     */
    public function batchAdd($a,$b,$expected)
    {
        $this->assertEquals($expected,$a+$b);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }

在例子中,為batchAdd方法宣告additionProvider為方法的資料供給器,batchAdd方法會對提供的陣列中,都執行相同的斷言

實踐 Laravel phpunit

基境(fixture)

在編寫測試時,最費時的部分之一是編寫程式碼來將整個場景設定成某個已知的狀態,並在測試結束後將其復原到初始狀態。這個已知的狀態稱為測試的 基境(fixture)

PHPUnit 支援共享建立基境的程式碼。在執行某個測試方法前,會呼叫一個名叫 setUp() 的模板方法。setUp() 是建立測試所用物件的地方。當測試方法執行結束後,不管是成功還是失敗,都會呼叫另外一個名叫 tearDown() 的模板方法。tearDown() 是清理測試所用物件的地方。

class CalcTest extends TestCase
{
    protected $base;
    public function setUp(): void
    {
        $this->base = 5;
    }

    /**
     * @test
     */
    public function equal()
    {
        $this->assertEquals(5,$this->base);
    }
}

在測試類中定義setUp方法後,在執行測試方法前,會先呼叫setUp()方法,有點類似construct()方法,在建立類後,先執行construct()

實踐

以上述講述的內容,通過一個實際的例子演示編寫相關測試用例

在實際開發電商專案中,我遇到需要開發活動優惠的需求,我需要在後臺新增一個活動,並將需要參加活動的商品新增到活動內並設定折扣,在使用者支付和購物車頁面顯示優惠後的價格,我根據這個需求簡單的編寫了相關的測試用例。

首先執行命令php artisan make:test ActivityTest --unit生成單元測試類

單元測試類程式碼如下:

以下例子中,我先定義了setUp方法,這個方法實際上生成活動的資料,然後在check_user_buy_goods_is_contain_activity_goods 方法中判斷是否包含活動商品,calc_buy_goods_discount_price 方法依賴於check_user_buy_goods_is_contain_activity_goods方法。在定義測試方法時,方法命名儘量語義化

class ActivityTest extends TestCase
{
    protected $activityGoods;
    protected $discount;
    /**
     * 初始化獲取活動詳情,這裡簡單賦值
     */
    public function setUp(): void
    {
        //參與本次活動的商品id
        $this->activityGoods = ['1','2','3'];
        //活動的折扣
        $this->discount = 0.8;
    }

    /**
     * 檢查使用者購買商品是否包含活動商品
     * @test
     */
    public function check_user_buy_goods_is_contain_activity_goods()
    {
        //id為商品id,price為商品價格,num為購買數量
        $cart = [
            ['id'=>1,'price'=>10,'num'=>1],
            ['id'=>2,'price'=>15,'num'=>2]
        ];

        $cartId = collect($cart)->pluck('id')->toArray();
        //是否存在活動商品
        $hasActivity = array_intersect($cartId,$this->activityGoods);
        $this->assertNotCount(0,$hasActivity);
        return ['cart'=>$cart,'hasActivity'=>$hasActivity];
    }

    /**
     * 計算購物車商品參與活動後的價格
     * @depends check_user_buy_goods_is_contain_activity_goods
     * @test
     * @param $info
     */
    public function calc_buy_goods_discount_price($info)
    {
        $cart = $info['cart'];
        $hasActivity = $info['hasActivity'];
        $total = 0;
        //計算價格
        foreach ($cart as $k=>$v){
            $price = $v['price'];
            $num = $v['num'];
            if (in_array($v['id'],$hasActivity)){
                $price = $price * $this->discount;
            }
            $total += $price*$num;
        }
        //判斷折扣後的價格
        $this->assertEquals(32,$total);
    }
}

執行phpunit --filter=ActivityTest命令,輸出以下內容

實踐 Laravel phpunit

斷言測試執行成功,我們可以多測試幾組資料去驗證程式碼是否正確。

使用資料提供器

這裡我直接使用calc_cart_total計算購物車總價,通過資料提供器提供資料,並驗證斷言是否正確

class ActivityTest extends TestCase
{
    protected $activityGoods;
    protected $discount;

    /**
     * 初始化獲取活動詳情,這裡簡單賦值
     */
    public function setUp(): void
    {
        //參與本次活動的商品id
        $this->activityGoods = ['1', '2', '3'];
        //活動的折扣
        $this->discount = 0.8;
    }

    /**
     * @dataProvider additionProvider
     * 計算購物車總價
     * @test
     * @param $input
     * @param $output
     */
    public function calc_cart_total($input,$output)
    {
        $cart = $input;
        $total = 0;
        $cartId = collect($cart)->pluck('id')->toArray();
        $hasActivity = array_intersect($cartId, $this->activityGoods);
        //計算價格
        foreach ($cart as $k => $v) {
            $price = $v['price'];
            $num = $v['num'];
            if (in_array($v['id'], $hasActivity)) {
                $price = $price * $this->discount;
            }
            $total += $price * $num;
        }
        //判斷折扣後的價格
        $this->assertEquals($output, $total);
    }

    /**
     * 資料提供器
     * @return array[]
     */
    public function additionProvider()
    {
        return [
            [
                [
                    ['id' => 1, 'price' => 10, 'num' => 1],
                    ['id' => 2, 'price' => 15, 'num' => 2]
                ],
                32
            ],
            [
                [
                    ['id' => 1 ,'price' => 10 ,'num'=>1],
                    ['id' => 4 ,'price' => 20 ,'num'=>5]
                ],
                108
            ],
            [
                [
                    ['id' => 4 ,'price' => 10 ,'num'=>1],
                    ['id' => 5 ,'price' => 20 ,'num'=>5]
                ],
                110
            ]
        ];
    }
}

執行phpunit --filter=ActivityTest命令,輸出以下內容

實踐 Laravel phpunit

執行結果通過,說明程式碼執行正確

總結

目前這篇文章主要還都是使用phpunit進行簡單的單元測試,當然還有一些其他操作,laravel的功能測試一塊,後續再慢慢了解總結吧,也是希望自己在以後的開發過程中,能夠更加註意測試,不要老寫一些不必要的bug。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章