天天看點

一份經過時間檢驗的 Laravel PHPUnit 測試經驗分享

介紹

作為開發者我們可能都有過這樣的經曆:

  • 修複了一個注冊功能的 bug,結果把登入功能搞崩了,直到使用者回報才知道。
  • 新增功能或者修改代碼都束手束腳,生怕對項目造成破壞性影響。

而這些困境很大部分的原因在于項目缺少完善的測試,無法保障項目的健壯,接下來我們就來分享我司經過數年的實踐,錘煉出來的一套成熟的測試工藝。

利用 Laravel 提供的測試工具

Laravel 對請求進行了封裝,我們可以非常友善地在測試中發起各種請求,比如說測試使用者清單。 下面是 Laravel 自帶的示例代碼:

public function testBasicTest()
{
    $response = $this->get('/users');
​
    $response->assertStatus(200);
}
           

這裡有幾個問題:

  • 在運作測試前需要建立資料表。
  • 需要填充一些測試資料,否則測試空的使用者清單沒有太大意義。
  • 希望能驗證請求傳回的 response 資料是否符合預期。

下面我們就來一一解決以上問題。

Migrations

Laravel 提供了 migration 來建立和更改資料表結構,在測試啟動前可以先運作

migrate

指令。随着時間的推移,表結構變化積累得越多,migration 耗時就會越長。

Migration 對資料庫做增量修改的做法并沒有給測試帶來任何好處,相反,在運作測試之前如果能對資料庫進行全量構造,性能會提高很多。是以我們建立了自動化工具,對于每次添加新的 migration,都會自動生成全量的表結構描述檔案用于測試。

Seeding

有了上一步的 migration 生成的資料表,我們還需要往資料表中插入測試資料,Laravel 再次貼心的提供了 seeding 功能,但是 seed 檔案是通過手寫的數組來存放資料,比如像下面這個樣子:

<?php
​
use Illuminate\Database\Seeder;
use DB;
​
class UsersTableSeeder extends Seeder
{
    public function run()
    {
        DB::table('users')->insert([
            'name' => 'baijunyao',
        ]);
    }
}
           

我們的業務邏輯比較複雜,需要多套測試資料集,每套測試資料集中的資料又各有依賴,是以我們維護數套 YAML 格式存儲的資料集,并實作了

YamlSeeder

來将 YAML 資料用于 seed 測試資料庫。

users:
  - name: baijunyao
           

Assert Response

Laravel 提供了一系列的

assert

方法,但是一個一個的手動調用這些方法比較繁瑣,手動寫死 response 的資料就更加痛苦了。

public function testBasicTest()
{
    $response = $this->get('/users');
​
    $response->assertStatus(200);
    $response->assertJson([
        [
            'name' => 'baijunyao',
        ]
    ]);
}
           

我們的方案是開發了一套自動 snapshot 測試工具。我們的測試用例有兩種模式運作測試:建立 snapshot 和執行測試。前者可以自動捕捉所有 API response 并以 JSON 格式存儲,後者則會将該次測試輸出和 snapshot 進行比較以做斷言。以 JSON 格式存在的 snapshot 結果集随代碼一同 commit 到代碼倉庫中,可以友善地追蹤每次的代碼修改對 response 造成的影響。

{
    "status_code": 200,
    "headers": {
        "cache-control": [
            "no-cache, private"
        ],
        "date": "Mon, 06-Jan-2020 00:00:00 GMT",
        "content-type": [
            "application/json"
        ],
        "x-ratelimit-limit": [
            60
        ],
        "x-ratelimit-remaining": [
            59
        ],
        "set-cookie": [
            "foo=bar; expires=Mon, 06-Jan-2020 01:00:00 GMT; max-age=3600; path=/; secure; httponly"
        ],
    },
    "content": [
        {
            "name": "baijunyao"
        }
    ]
}
           

這種比對整個 response 的方案中有一些細節需要注意,比如:

變量

有一些變量比如日期,可能造成每次的 response 都不一樣,我們可以使用 Carbon 在測試模式中設定一個固定的目前日期。

Carbon::setTestNow(Carbon::create(2020, 1, 1, 0, 0, 0));
           

對于其他一些變量資料采用Mockery,無法 mock 的則忽略變量部分。

重置測試資料

app('db')->listen(function ($query) {
    // ...
});
           

資料庫

結語