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

RightCapital發表於2020-05-18

介紹

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

  • Laravel v7 都已經發布了,而自己維護的專案仍然是公司祖傳的 v5.3,遲遲不敢升級。
  • 修復了一個註冊功能的 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 的則忽略變數部分。

重置測試資料

因為新增資料的功能測試會真實的向資料庫中插入一條資料,為了清理髒資料,Laravel 提供了 Illuminate\Foundation\Testing\RefreshDatabase trait,它使用的是資料庫的事務,存在的問題是資料雖然重置了,但是並沒有重置自增型的主鍵,會造成每次執行測試時 id 不確定的問題。我們的解決方案是通過 DB 監聽執行的 SQL,然後通過 TRUNCATE 來清理被汙染的資料。

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

資料庫

Laravel 預設是使用 SQLite 作為資料庫,我們使用的則是 MySQL,在每次啟動測試前建立一個臨時資料庫,測試結束後銷燬這個臨時的資料庫,藉助於 Docker 我們可以最大程度的保障測試環境跟生產環境的一致性。

結語

編寫測試程式碼是一個必要而且有價值的工作內容,它可以讓我們有底氣的進行功能開發,快速的進行迭代,希望我們的測試方案對您有所幫助,圍繞著測試我們開發了一系列的擴充套件包來簡化我們編寫測試程式碼的工作,如果對我們的測試方案有興趣,歡迎留言,我們會逐步進行更深入更詳細的講解並開源這些工具。


請關注我們的微信公眾號「RightCapital」

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

歡迎關注我們的微信公眾號「RightCapital」

相關文章