如何正確在 Laravel 撰寫 PHPUnit 單元測試(Unit Test)

本篇文章內容主要參考自 LaracastsLaravel 5.4 From Scratch: Testing 101,並且替換掉舊有寫法,改寫成新版(Laravel 5.6)的版本。

我們會撰寫一個簡單的 Unit Test 測試,可以在獨立的測試資料庫中測試 model 操作 CRUD,同時不會因為測試 Create 操作而造成資料庫無限肥大下去。


建立測試案例

先建立一個測試案例,本篇以單元測試為例建立一個 PostTest.php。

# Create a test in the Unit directory...
php artisan make:test PostTest --unit

規劃測試內容

例如我們想要測試以下 Post model 的 method 是否正常:

# app/Post.php
public static function archives()
{
    return static::selectRaw('year(created_at) year, monthname(created_at) month, count(*) published')
        ->groupBy('year', 'month')
        ->orderByRaw('min(created_at) desc')
        ->get()
        ->toArray();
}

我們可以先依據 Given-When-Then 格式來撰寫註解,規劃好我們要寫的測試內容。關於 Given-When-Then 可以參考本篇文末 延伸閱讀 段落的整理。

# tests/Unit/PostTest.php
public function testArchives()
{
    // Given I have two records in the database that art posts,
    // and each one is posted a month apart.

    // When I fetch the archives.

    // Then the response should be in the proper format.
}

建立測試資料

在這裡,我們需要兩筆資料來測試。這時我們可以使用 Model Factories 來產生我們需要的測試資料到資料庫中。關於 Model Factories 以及資料庫測試的詳細說明,可以參考 官方文件道場的翻譯文件

我們先產生一個 Post 的 Factory

php artisan make:factory PostFactory --model=Post

Factory 可以配合 Faker 自動隨機產生我們需要的內容到資料庫中:

# database/factories/PostFactory.php
$factory->define(App\Post::class, function (Faker $faker) {
    return [
        'user_id' => function () {
	        return factory(App\User::class)->create()->id;
        },
        'title' => $faker->sentence,
        'body' => $faker->paragraph
    ];
});

Faker 可以產生非常多樣化的假資料,可以到 fzaninotto/Faker 看看各種稀奇古怪的假資料。

完成 Factory 後,我們就可以拿來用在測試案例的 Given 段落中,建立兩筆 Post,並且第二篇手動指定建立時間為一個月前。When 段落則是執行我們要測試的 method。Then 段落比較執行結果與我們預期的相不相同。

關於 Then 段落可以使用的其他斷言方法,可以查看本篇文末 延伸閱讀 段落的整理。

於是,我們的測試就長得像下方這樣:

# tests/Unit/PostTest.php
public function testArchives()
{
    // Given I have two records in the database that art posts,
    // and each one is posted a month apart.
	$first = factory(Post::class)->create();
	$second = factory(Post::class)->create([
	   'created_at' => \Carbon\Carbon::now()->subMonth()
	]);

    // When I fetch the archives.
    $posts = Post::archives();

    // Then the response should be in the proper format.
    $this->assertCount(2, $posts);
}

我們先簡單測試是否有抓到兩篇文章就好,之後再來改良它。

試試看執行測試,如果 posts 資料表真的只有兩篇文,archives method 也正確執行的話,我們就會看到綠燈:

phpunit tests/Unit/PostTest.php

但是有一個問題,我們每次執行測試會建立資料在我們的資料庫中,這樣會跟其他的資料混在一起吧?如果我們 local 的 posts 表本身就超過 2 篇這個測試就不會通過了?


建立測試資料庫

為了避免測試資料與一般資料混在一起,我們可以建立一個測試資料庫。以 MySQL 為例,先登入 MySQL:

mysql -uroot -p

然後新增一個資料庫,例如這裡我們命名為「blog_testing」:

create database blog_testing;

接著我們可以到專案根目錄的 phpunit.xml 指定測試時要用的資料庫。這個檔案會在執行測試時暫時覆寫 Laravel 的環境變數,像是將 APP_ENV 改為「testing」。我們在 php 區塊新增一個 DB_DATABASE 環境變數,並指定為「blog_testing」:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="QUEUE_DRIVER" value="sync"/>
    <env name="DB_DATABASE" value="blog_testing"/>
</php>

這時如果我們執行測試,會發現噴一堆錯誤,因為還沒有針對 blog_testing 做 migration 啊!那怎麼做才能對測試資料庫做 migration 呢?比較正規的做法是在 config/database.php 中新增一個全新的 connection,但這樣為了 migration 大費周章有點麻煩。

另一個比較 簡便的方法,就是暫時在 .env 中把 DB_DATABASE 也改成「blog_testing」,然後執行 :

php artisan migrate

這樣就很快速的搞定了。記得要把 .env 改回來啊!

然後我們再試試看執行測試,如果順利我們就會看到綠燈:

phpunit tests/Unit/PostTest.php

但是又有一個問題,我們每次執行測試都會多兩筆資料,舊的 faker 資料又用不到了很礙眼啊!總不能每次都要去資料庫手動刪除吧?


在每次測試後重置資料庫

在 Laravel 5.4 以前,我們可以使用 DatabaseTransation 這個 Trait 來協助我們 rollback 成執行前的樣子。在 Laravel 5.5 以後則是改用更為強大的 RefreshDatabase 這個 Trait。詳細的比較說明可以參考 Laracasts 的 What’s New in Laravel 5.5: The RefreshDatabase Trait

所以只要在你的測試類別裡的第一行加上,Laravel 就會幫你處理所有事情:

use RefreshDatabase;

詳細可以參考 官方文件 或是 道場的翻譯文件


改進測試案例

現在,我們想要更精準地撰寫測試案例,例如想要知道 When 區段撈出來的 $posts 資料真的是我們在 Given 建立的資料可以怎麼做呢?

以這次的測試為例,我們可以在 Then 區段改成用 assertEquals 來判斷:

$this->assertEquals([
    [
        "year" => $first->created_at->format('Y'),
        "month" => $first->created_at->format('F'),
        "published" => 1
    ],
    [
        "year" => $second->created_at->format('Y'),
        "month" => $second->created_at->format('F'),
        "published" => 1
    ],
], $posts);

這樣就可以確認我們在 When 區段所撈取的就是 Given 區段建立的文章了。


參考資料與延伸閱讀

Laracasts

Given-When-Then

斷言方法