如何正確在 Laravel 撰寫 PHPUnit 單元測試(Unit Test)
本篇文章內容主要參考自 Laracasts 的 Laravel 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 區段建立的文章了。