簡單介紹 PHP 測試框架 Pest (下)
繼上回簡單介紹 PHP 測試框架 - Pest (上) 後,讓我們緊接著繼續看看 Pest 的其他實用功能。
使用 Datasets 測試多組資料
如果你想要在一個流程測試中,測試多筆資料,你會怎麼做呢?
雖然我們可以把測試拆開,為每筆資料都寫一個測試,但因為測試邏輯與流程是一樣的,如果每一筆資料都寫一個測試,會讓程式碼顯得冗長且非常多餘。
遇到這樣的情況,可以考慮使用 Datasets。
我們可以在匿名函數中放入一個參數 $email
,並呼叫 with()
方法代入一個有多筆信箱資料的陣列。
it('can store a contact', function ($firstName) {
login()->post('/contacts', [
'first_name' => $firstName, // 代入 Datasets 資料
'last_name' => faker()->lastName,
'email' => faker()->email,
// ...
])
->assertRedirect('/contacts')
->assertSessionHas('success', 'Contact created');
})->with([
'Allen',
'"Lisa"',
]);
此時這個測試就會被執行兩次,而這兩次都會測試不同的信箱資料。
執行測試之後也會顯示為兩個測試。
PASS Tests\Feature\Contacts\StoreTest
✓ it can store a contact with (Allen)
✓ it can store a contact with ('"Lisa"')
Datasets 也可以帶入多個參數。
it('can store a contact', function ($firstName, $email) {
login()->post('/contacts', [
'first_name' => $firstName,
'last_name' => faker()->lastName,
'email' => $email,
// ...
])
->assertRedirect('/contacts')
->assertSessionHas('success', 'Contact created');
})->with([
['John', faker()->email],
['Hellen', '"test"@email.com'],
]);
可以使用參數拆包的方式,如果沒有在 Datasets 設定資料,就使用預設的資料。
it('can store a contact', function (array $data) {
login()->post('/contacts', [
...[
'first_name' => faker()->firstName,
'last_name' => faker()->lastName,
'email' => faker()->email,
// ...
],
...$data
])
->assertRedirect('/contacts')
->assertSessionHas('success', 'Contact created');
})->with([
[[]],
[['email' => faker()->email, 'first_name' => 'John']],
[['email' => '"test"@email.com', 'first_name' => 'Hellen']],
]);
可以在 Datasets 中加上 key 值。
it('can store a contact', function (array $data) {
login()->post('/contacts', [
...[
'first_name' => faker()->firstName,
'last_name' => faker()->lastName,
'email' => faker()->email,
// ...
],
...$data
])
->assertRedirect('/contacts')
->assertSessionHas('success', 'Contact created');
})->with([
'empty' => [[]],
'data 1' => [['email' => faker()->email, 'first_name' => 'John']],
'data 2' => [['email' => '"test"@email.com', 'first_name' => 'Hellen']],
]);
這樣測試結果就會顯示你所設定的 key 名稱。
PASS Tests\Feature\Contacts\StoreTest
✓ it can store a contact with data set "empty"
✓ it can store a contact with data set "data 1"
✓ it can store a contact with data set "data 2"
設定 Shared Datasets
可以使用 Laravel 中 artisan
的指令 pest:dataset
生成一個 Shared Datasets。
php artisan pest:dataset Emails
這個指令會生成一個 tests/Datasets/Emails.php
,我們可以在其中定義一個 Datasets。
dataset('valid emails', function () {
return ['email A', 'email B']
});
之後就可以在測試中使用這個 Shared Datasets。
it('can store a contact', function ($email) {
login()->post('/contacts', [
'first_name' => faker()->firstName,
'last_name' => faker()->lastName,
'email' => $email,
// ...
])
->assertRedirect('/contacts')
->assertSessionHas('success', 'Contact created');
})->with('valid emails'); // 使用 Shared Datasets 資料
Combined Datasets
你可以組合多個 Datasets,測試更多種組合資料。
例如我想測試一個上傳圖片後顯示圖片規格的功能,除了測試圖片類型,例如 jpg、png 或是 webp,每個類型也想測試不同的大小尺寸。
use Illuminate\Support\Facades\Storage;
it('can show supported image formats and options', function ($path, $options) {
Storage::fake()->put($path, file_get_contents(__DIR__ . "/../../fixtures/{$path}"))
$response = $this->get(route('image', ['path' => $path, ...$options]));
$response->assertOk();
expect($response->streamedContent())->not->toBeEmpty()->toBeString();
})->with([
['example.jpg', ['w' => 10, 'h' => 10, 'fit' => 'crop']],
['example.jpg', ['w' => 40, 'h' => 40, 'fit' => 'crop']],
['example.jpg', ['w' => 50, 'h' => 50, 'fit' => 'crop']],
['example.png', ['w' => 10, 'h' => 10, 'fit' => 'crop']],
['example.png', ['w' => 40, 'h' => 40, 'fit' => 'crop']],
['example.png', ['w' => 50, 'h' => 50, 'fit' => 'crop']],
['example.webp', ['w' => 10, 'h' => 10, 'fit' => 'crop']],
['example.webp', ['w' => 40, 'h' => 40, 'fit' => 'crop']],
['example.webp', ['w' => 50, 'h' => 50, 'fit' => 'crop']],
]);
上面的寫法重複度有些高,為了測試 10、40 與 50 的尺寸,我們每個圖片都要特地寫三種尺寸,這時候就可以考慮使用組合 Datasets。
use Illuminate\Support\Facades\Storage;
it('can show supported image formats and options', function ($path, $options) {
Storage::fake()->put($path, file_get_contents(__DIR__ . "/../../fixtures/{$path}"))
$response = $this->get(route('image', ['path' => $path, ...$options]));
$response->assertOk();
expect($response->streamedContent())->not->toBeEmpty()->toBeString();
})->with([
'example.jpg',
'example.png',
'example.webp',
])->with([
['w' => 40, 'h' => 40, 'fit' => 'crop'],
['w' => 50, 'h' => 50, 'fit' => 'crop'],
['w' => 10, 'h' => 10, 'fit' => 'crop'],
]);
顯示的測試結果如下所示。
PASS Tests\Feature\Image\ShowTest
✓ it can show supported image formats and options with ('example.jpg') / (array(40, 40, 'crop'))
✓ it can show supported image formats and options with ('example.jpg') / (array(50, 50, 'crop'))
✓ it can show supported image formats and options with ('example.jpg') / (array(10, 10, 'crop'))
✓ it can show supported image formats and options with ('example.png') / (array(40, 40, 'crop'))
✓ it can show supported image formats and options with ('example.png') / (array(50, 50, 'crop'))
✓ it can show supported image formats and options with ('example.png') / (array(10, 10, 'crop'))
✓ it can show supported image formats and options with ('example.webp') / (array(40, 40, 'crop'))
✓ it can show supported image formats and options with ('example.webp') / (array(50, 50, 'crop'))
✓ it can show supported image formats and options with ('example.webp') / (array(10, 10, 'crop'))
使用 Group 幫測試建立群組
你可以使用 group()
將多個測試放進一個群組中。
it('can validate an email', function () {
$rule = new IsValidEmailAddress();
expect($rule->passes('email', '[email protected]'))->toBeTrue();
})->group('validation');
這樣執行測試時就可以使用 --group
來執行指定群組的測試。
group() 是可以跨檔案的,你可以把位於不同檔案的測試放進同一個群組中。
php artisan test --group=validation
如果檔案內全部測試都屬於同一個群組,我們在檔案的上方使用下面的語法,這樣該檔案中的全部測試都會被加入 validation
群組中。
uses()->group('validation');
如果不想要執行某群組的測試,可以在指令中使用 --exclude
。
php artisan test --exclude=validation
斷定是否會拋出例外 (Exception)
如果要測試一個流程在遭遇問題時是否會拋出例外 (Exception),斷定中也有 expectException()
的方法可以使用。
it('can validate an email', function () {
// 如果預計這個測試會拋出例外,我們可以使用這個方法來進行斷定
// 注意這個方法必須放在測試中的最上方
$this->expectException(InvalidArgumentException::class);
$rule = new IsValidEmailAddress();
$rule->passes('email', 123);
});
Pest 為這種情況提供一個更直覺的方法 throws()
。
it('can validate an email', function () {
$rule = new IsValidEmailAddress();
$rule->passes('email', 123);
})
->throws(InvalidArgumentException::class);
除了斷定丟出哪一種例外,也可以對例外的錯誤訊息進行斷定,就算例外的類型對上了,只要訊息不對,斷定還是會失敗。
it('can validate an email', function () {
$rule = new IsValidEmailAddress();
$rule->passes('email', 123);
})
->throws(InvalidArgumentException::class, 'The value must be a string');
使用 Skip 根據情況跳過測試
Pest 中可以使用 skip()
方法跳過測試。
it('can validate an email', function () {
$rule = new IsValidEmailAddress();
expect($rule->passes('email', '[email protected]'))->toBeTrue();
})
// skip 接受一個字串用來顯示為什麼跳過的訊息
->skip('we don\'t need this test anymore');
這樣執行測試時,該測試就會被跳過,並在測試結果上顯示為什麼跳過的訊息。
WARN Tests\Feature\ExampleTest
- it can validate an email → we don't need this test anymore
skip()
可以傳入一個判斷式,可以根據情況來決定要不要執行這個測試。
it('can validate an email', function () {
$rule = new IsValidEmailAddress();
expect($rule->passes('email', '[email protected]'))->toBeTrue();
})
// 可以用來根據環境來決定是否跳過測試
->skip(getenv('SKIP_TESTS') ?? false, 'we don\'t need this test in this condition');
注意 skip()
的判斷是在 Laravel 啟動之前,所以沒有辦法直接使用 Laravel 提供的方法。
it('can validate an email', function () {
$rule = new IsValidEmailAddress();
expect($rule->passes('email', '[email protected]'))->toBeTrue();
})
// error
->skip(config('app.name') === 'foo', 'we don\'t need this test in this condition');
想使用 Laravel 提供的方法,就必須將其放入匿名函數中。
it('can validate an email', function () {
$rule = new IsValidEmailAddress();
expect($rule->passes('email', '[email protected]'))->toBeTrue();
})
// make config() be executed after laravel booted
->skip(fn () => config('app.name') === 'foo', 'we don\'t need this test in this condition');
查看測試覆蓋度 (Code Coverage)
Laravel 在 test
指令中可以使用 --coverage
顯示程式碼的測試覆蓋度。
測試覆蓋程度這個功能由 PHP 的擴展 Xdebug 所提供,所以使用之前必須確認有安裝 Xdebug。
pecl install xdebug
並在 Xdebug 的設定檔案中開啟 coverage mode。
xdebug.mode=coverage
如果你是使用 Laravel Sail 開發環境的話,需要在 .env
檔案中加上一個變數,重新建立容器之後,容器中的 Xdebug 就會開啟 coverage mode。
SAIL_XDEBUG_MODE=coverage
設定好之後就可以在執行測試時,使用 --coverage
來顯示測試覆蓋度。
php artisan test --coverage
你可以自己定義最低的測試覆蓋程度應該是多少百分比,低於這個百分比,指令就會拋出 exit(1)
的錯誤,可以在 CI (Continuous Integration) 中用來中斷布署流程的進行。
php artisan test --coverage --min=40
指令執行後的顯示結果如下 (前半段省略)。
...
Notifications/PostComment ................................................................................. 100.0 %
Policies/CommentPolicy .................................................................................... 100.0 %
Policies/Policy ........................................................................................... 100.0 %
Policies/PostPolicy ....................................................................................... 100.0 %
Policies/UserPolicy ....................................................................................... 100.0 %
Providers/AppServiceProvider .............................................................................. 100.0 %
Providers/AuthServiceProvider ............................................................................. 100.0 %
Providers/BroadcastServiceProvider ........................................................................... 0.0 %
Providers/EventServiceProvider ............................................................................ 100.0 %
Providers/RouteServiceProvider ............................................................................ 100.0 %
Rules/MatchOldPassword .................................................................................... 100.0 %
Rules/Recaptcha .............................................................................................. 0.0 %
Services/FileService ...................................................................................... 100.0 %
Services/FormatTransferService 17 ........................................................................... 83.3 %
Services/PostService 46, 57..75, 62..109 .................................................................... 40.5 %
Services/SettingService ................................................................................... 100.0 %
View/Components/AppLayout ................................................................................. 100.0 %
helpers 3, 30..34 ........................................................................................... 50.0 %
Total Coverage .............................................................................................. 80.6 %
測試覆蓋度可以讓你更清楚的知道還有哪邊的程式碼沒有測試到,是個相當方便且重要的功能。
結語
Pest 的一些功能介紹就到這邊 (其實上述官方文件都有寫到,真要說還不如看文件),相信各位閱讀後會發現 Pest 測試方式與 PHPUnit 大同小異,只是 Pest 提供更多實用的方法協助我們進行測試,因此在轉換上幾乎沒有任何負擔!
使用程式寫出我們想要的功能固然有趣,但能確保功能沒有問題的測試也同樣令人著迷。希望大家都能更愉快地寫出更棒的測試。