簡介
Laravel 提供豐富的 API 讓你能模擬 HTTP 請求並驗證回應。 不用真的架設 HTTP 伺服器,就能在測試中重現對應用程式的請求。<?php
test('應用程式回傳正常回應', function () {
$response = $this->get('/');
$response->assertStatus(200);
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
get() 方法對應用程式送出 GET 請求,assertStatus() 驗證回應的 HTTP 狀態碼。
執行測試期間,CSRF 中介軟體會自動停用,不必在測試中手動關閉。
建立請求
在測試中可用get、post、put、patch、delete 送出請求。
這些方法不會實際發送網路請求,而是在應用程式內部模擬。
回傳值為 Illuminate\Testing\TestResponse 實例,提供大量斷言方法。
<?php
test('基本的請求', function () {
$response = $this->get('/');
$response->assertStatus(200);
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_a_basic_request(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
建議每個測試方法內原則上僅送出一次請求。同一測試中發送多次請求可能導致非預期行為。
自訂請求標頭
用withHeaders() 自訂請求標頭。
<?php
test('附標頭的請求', function () {
$response = $this->withHeaders([
'X-Header' => 'Value',
])->post('/user', ['name' => 'Sally']);
$response->assertStatus(201);
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_interacting_with_headers(): void
{
$response = $this->withHeaders([
'X-Header' => 'Value',
])->post('/user', ['name' => 'Sally']);
$response->assertStatus(201);
}
}
Cookie
以withCookie() 或 withCookies() 在請求前設定 cookie 值。
<?php
test('附 cookie 的請求', function () {
$response = $this->withCookie('color', 'blue')->get('/');
$response = $this->withCookies([
'color' => 'blue',
'name' => 'Taylor',
])->get('/');
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_interacting_with_cookies(): void
{
$response = $this->withCookie('color', 'blue')->get('/');
$response = $this->withCookies([
'color' => 'blue',
'name' => 'Taylor',
])->get('/');
}
}
Session 與認證
用withSession() 在請求前設定 session 資料。
<?php
test('附 session 的請求', function () {
$response = $this->withSession(['banned' => false])->get('/');
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_interacting_with_the_session(): void
{
$response = $this->withSession(['banned' => false])->get('/');
}
}
actingAs() 可以以已認證使用者身份送出請求,常搭配模型工廠使用。
<?php
use App\Models\User;
test('需要認證的動作', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)
->withSession(['banned' => false])
->get('/');
});
<?php
namespace Tests\Feature;
use App\Models\User;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_an_action_that_requires_authentication(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->withSession(['banned' => false])
->get('/');
}
}
actingAs() 的第 2 個參數指定 guard 名稱,可用該 guard 認證。該 guard 在測試期間會成為預設。
$this->actingAs($user, 'web');
actingAsGuest()。
$this->actingAsGuest();
除錯回應
若想在測試中檢視回應內容,可用dump、dumpHeaders、dumpSession。
<?php
test('回應除錯', function () {
$response = $this->get('/');
$response->dump();
$response->dumpHeaders();
$response->dumpSession();
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_debug_response(): void
{
$response = $this->get('/');
$response->dump();
$response->dumpHeaders();
$response->dumpSession();
}
}
dd、ddHeaders、ddBody、ddJson、ddSession。
$response->dd();
$response->ddHeaders();
$response->ddBody();
$response->ddJson();
$response->ddSession();
例外測試
要測試是否會拋出特定例外,可用Exceptions Facade。
<?php
use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Exceptions;
test('拋出例外', function () {
Exceptions::fake();
$response = $this->get('/order/1');
// 確認例外被拋出
Exceptions::assertReported(InvalidOrderException::class);
// 確認例外內容
Exceptions::assertReported(function (InvalidOrderException $e) {
return $e->getMessage() === 'The order was invalid.';
});
});
<?php
namespace Tests\Feature;
use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Exceptions;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_exception_is_thrown(): void
{
Exceptions::fake();
$response = $this->get('/');
Exceptions::assertReported(InvalidOrderException::class);
Exceptions::assertReported(function (InvalidOrderException $e) {
return $e->getMessage() === 'The order was invalid.';
});
}
}
assertNotReported 或 assertNothingReported。
Exceptions::assertNotReported(InvalidOrderException::class);
Exceptions::assertNothingReported();
withoutExceptionHandling()。
$response = $this->withoutExceptionHandling()->get('/');
assertThrows()。
$this->assertThrows(
fn () => (new ProcessOrder)->execute(),
OrderInvalid::class
);
// 若要檢查例外內容
$this->assertThrows(
fn () => (new ProcessOrder)->execute(),
fn (OrderInvalid $e) => $e->orderId() === 123
);
assertDoesntThrow()。
$this->assertDoesntThrow(fn () => (new ProcessOrder)->execute());
JSON API 的測試
Laravel 提供多種 JSON API 測試輔助方法。json、getJson、postJson、putJson、patchJson、deleteJson、optionsJson 皆可送出 JSON 請求。
<?php
test('送出 API 請求', function () {
$response = $this->postJson('/api/user', ['name' => 'Sally']);
$response
->assertStatus(201)
->assertJson([
'created' => true,
]);
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_making_an_api_request(): void
{
$response = $this->postJson('/api/user', ['name' => 'Sally']);
$response
->assertStatus(201)
->assertJson([
'created' => true,
]);
}
}
expect($response['created'])->toBeTrue();
$this->assertTrue($response['created']);
assertJson() 會把回應轉為陣列,並驗證所指定陣列是否包含在 JSON 中。JSON 中其他屬性即使存在,只要指定的片段有出現,測試就會通過。完全比對的斷言
用assertExactJson() 可驗證回傳 JSON 與指定陣列完全一致。
<?php
test('完全比對的 JSON 斷言', function () {
$response = $this->postJson('/user', ['name' => 'Sally']);
$response
->assertStatus(201)
->assertExactJson([
'created' => true,
]);
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_asserting_an_exact_json_match(): void
{
$response = $this->postJson('/user', ['name' => 'Sally']);
$response
->assertStatus(201)
->assertExactJson([
'created' => true,
]);
}
}
JSON path 的斷言
用assertJsonPath() 檢驗特定路徑上的資料。
<?php
test('JSON path 斷言', function () {
$response = $this->postJson('/user', ['name' => 'Sally']);
$response
->assertStatus(201)
->assertJsonPath('team.owner.name', 'Darian');
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_asserting_a_json_paths_value(): void
{
$response = $this->postJson('/user', ['name' => 'Sally']);
$response
->assertStatus(201)
->assertJsonPath('team.owner.name', 'Darian');
}
}
$response->assertJsonPath('team.owner.name', fn (string $name) => strlen($name) >= 3);
流暢的 JSON 測試
assertJson() 傳入閉包時,可用 AssertableJson 實例以流暢方式撰寫斷言。
use Illuminate\Testing\Fluent\AssertableJson;
test('流暢的 JSON 測試', function () {
$response = $this->getJson('/users/1');
$response
->assertJson(fn (AssertableJson $json) =>
$json->where('id', 1)
->where('name', 'Victoria Faith')
->where('email', fn (string $email) => str($email)->is('[email protected]'))
->whereNot('status', 'pending')
->missing('password')
->etc()
);
});
use Illuminate\Testing\Fluent\AssertableJson;
public function test_fluent_json(): void
{
$response = $this->getJson('/users/1');
$response
->assertJson(fn (AssertableJson $json) =>
$json->where('id', 1)
->where('name', 'Victoria Faith')
->where('email', fn (string $email) => str($email)->is('[email protected]'))
->whereNot('status', 'pending')
->missing('password')
->etc()
);
}
etc() 方法允許存在非斷言中的其他屬性。若未加 etc(),一旦有未斷言的屬性存在,測試會失敗。這可避免無意間將機密資訊納入回應。has() 與 missing()。
$response->assertJson(fn (AssertableJson $json) =>
$json->has('data')
->missing('message')
);
hasAll() 或 missingAll()。
$response->assertJson(fn (AssertableJson $json) =>
$json->hasAll(['status', 'data'])
->missingAll(['message', 'code'])
);
JSON 集合的斷言
若路由回傳含多個項目的 JSON,可用has() 檢驗數量與內容。
$response
->assertJson(fn (AssertableJson $json) =>
$json->has(3)
->first(fn (AssertableJson $json) =>
$json->where('id', 1)
->where('name', 'Victoria Faith')
->missing('password')
->etc()
)
);
each()。
$response
->assertJson(fn (AssertableJson $json) =>
$json->has(3)
->each(fn (AssertableJson $json) =>
$json->whereType('id', 'integer')
->whereType('name', 'string')
->whereType('email', 'string')
->missing('password')
->etc()
)
);
JSON 的型別斷言
以whereType() 或 whereAllType() 檢查屬性型別。
$response->assertJson(fn (AssertableJson $json) =>
$json->whereType('id', 'integer')
->whereAllType([
'users.0.name' => 'string',
'meta' => 'array'
])
);
| 指定多個型別,其中之一符合即算通過。
$response->assertJson(fn (AssertableJson $json) =>
$json->whereType('name', 'string|null')
->whereType('id', ['string', 'integer'])
);
string、integer、double、boolean、array、null。
認證測試
用actingAs() 以已認證使用者身份送出請求。
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('已認證使用者可看儀表板', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertStatus(200);
});
test('未認證使用者會被重新導向', function () {
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
});
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DashboardTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_users_can_view_the_dashboard(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertStatus(200);
}
public function test_unauthenticated_users_are_redirected(): void
{
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
}
}
$this->actingAs($user, 'api');
使用者註冊流程測試範例
實際測試使用者註冊端點的範例:<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('使用者可以註冊', function () {
$response = $this->post('/register', [
'name' => 'Test User',
'email' => '[email protected]',
'password' => 'password',
'password_confirmation' => 'password',
]);
$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
});
test('重複 email 不可註冊', function () {
User::factory()->create(['email' => '[email protected]']);
$response = $this->post('/register', [
'name' => 'Test User',
'email' => '[email protected]',
'password' => 'password',
'password_confirmation' => 'password',
]);
$response->assertSessionHasErrors('email');
});
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class RegistrationTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_register(): void
{
$response = $this->post('/register', [
'name' => 'Test User',
'email' => '[email protected]',
'password' => 'password',
'password_confirmation' => 'password',
]);
$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
}
public function test_duplicate_email_cannot_register(): void
{
User::factory()->create(['email' => '[email protected]']);
$response = $this->post('/register', [
'name' => 'Test User',
'email' => '[email protected]',
'password' => 'password',
'password_confirmation' => 'password',
]);
$response->assertSessionHasErrors('email');
}
}
Session 測試
用withSession() 事先設定 session,用 assertSessionHas() 檢驗 session 是否有指定值。
<?php
test('驗證 session 值', function () {
$response = $this->withSession(['locale' => 'ja'])->get('/');
$response->assertSessionHas('locale', 'ja');
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class SessionTest extends TestCase
{
public function test_session_value(): void
{
$response = $this->withSession(['locale' => 'ja'])->get('/');
$response->assertSessionHas('locale', 'ja');
}
}
Session 斷言一覽
| 方法 | 說明 |
|---|---|
assertSessionHas($key, $value) | session 中有指定 key 與值 |
assertSessionHasAll([...]) | session 中有多個 key 與值 |
assertSessionHasErrors($keys) | session 中含指定的驗證錯誤 |
assertSessionHasErrorsIn($bag, $keys) | 指定錯誤袋中含錯誤 |
assertSessionHasNoErrors() | session 中無驗證錯誤 |
assertSessionDoesntHaveErrors() | session 中無驗證錯誤 |
assertSessionMissing($key) | session 中無指定 key |
檔案上傳的測試
以Illuminate\Http\UploadedFile 的 fake() 產生虛擬檔案或圖片。
搭配 Storage Facade 的 fake() 可輕鬆測試檔案上傳。
<?php
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
test('可以上傳頭像', function () {
Storage::fake('avatars');
$file = UploadedFile::fake()->image('avatar.jpg');
$response = $this->post('/avatar', [
'avatar' => $file,
]);
Storage::disk('avatars')->assertExists($file->hashName());
});
<?php
namespace Tests\Feature;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class AvatarTest extends TestCase
{
public function test_avatars_can_be_uploaded(): void
{
Storage::fake('avatars');
$file = UploadedFile::fake()->image('avatar.jpg');
$response = $this->post('/avatar', [
'avatar' => $file,
]);
Storage::disk('avatars')->assertExists($file->hashName());
}
}
assertMissing()。
Storage::disk('avatars')->assertMissing('missing.jpg');
自訂虛擬檔案
可指定圖片尺寸或檔案大小,適合測試驗證規則。// 指定寬 / 高 / 大小(大小單位為 KB)
UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100);
// 建立任意檔案類型
UploadedFile::fake()->create('document.pdf', $sizeInKilobytes);
// 明確指定 MIME 類型
UploadedFile::fake()->create(
'document.pdf', $sizeInKilobytes, 'application/pdf'
);
視圖測試
不需模擬 HTTP 請求,也能直接渲染視圖進行測試。view() 方法接收視圖名稱與可選的資料陣列,回傳 Illuminate\Testing\TestView 實例。
<?php
test('welcome 視圖可渲染', function () {
$view = $this->view('welcome', ['name' => 'Taylor']);
$view->assertSee('Taylor');
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ViewTest extends TestCase
{
public function test_a_welcome_view_can_be_rendered(): void
{
$view = $this->view('welcome', ['name' => 'Taylor']);
$view->assertSee('Taylor');
}
}
TestView 類別提供的斷言方法:
| 方法 | 說明 |
|---|---|
assertSee($value) | 視圖包含指定字串 |
assertSeeInOrder([...]) | 視圖依序包含指定字串 |
assertSeeText($value) | 視圖文字包含指定字串 |
assertSeeTextInOrder([...]) | 視圖文字依序包含指定字串 |
assertDontSee($value) | 視圖不含指定字串 |
assertDontSeeText($value) | 視圖文字不含指定字串 |
TestView 實例轉為字串。
$contents = (string) $this->view('welcome');
withViewErrors()。
$view = $this->withViewErrors([
'name' => ['請輸入有效名稱。']
])->view('form');
$view->assertSee('請輸入有效名稱。');
元件測試
以blade() 渲染原始 Blade 樣板字串。
$view = $this->blade(
'<x-component :name="$name" />',
['name' => 'Taylor']
);
$view->assertSee('Taylor');
component() 渲染 Blade 元件,回傳 Illuminate\Testing\TestComponent 實例。
$view = $this->component(Profile::class, ['name' => 'Taylor']);
$view->assertSee('Taylor');
回應斷言一覽
Illuminate\Testing\TestResponse 類別的主要斷言方法:
HTTP 狀態
| 方法 | 說明 |
|---|---|
assertOk() | 200 OK |
assertCreated() | 201 Created |
assertAccepted() | 202 Accepted |
assertNoContent($status = 204) | 204 No Content |
assertBadRequest() | 400 Bad Request |
assertUnauthorized() | 401 Unauthorized |
assertForbidden() | 403 Forbidden |
assertNotFound() | 404 Not Found |
assertUnprocessable() | 422 Unprocessable Entity |
assertTooManyRequests() | 429 Too Many Requests |
assertInternalServerError() | 500 Internal Server Error |
assertStatus($code) | 指定狀態碼 |
assertSuccessful() | 2xx 系列 |
assertClientError() | 4xx 系列 |
assertServerError() | 5xx 系列 |
重新導向
| 方法 | 說明 |
|---|---|
assertRedirect($uri) | 重新導向到指定 URI |
assertRedirectContains($string) | 重新導向 URI 包含指定字串 |
assertRedirectToRoute($name, $params) | 重新導向至指定路由 |
assertRedirectToSignedRoute($name, $params) | 重新導向至指定的簽章路由 |
assertRedirectBack() | 導回上一個 URL |
內容
| 方法 | 說明 |
|---|---|
assertSee($value, $escaped = true) | 回應含指定值 |
assertSeeInOrder(array $values) | 依序含指定值 |
assertSeeText($value) | 含指定文字 |
assertDontSee($value) | 不含指定值 |
assertDontSeeText($value) | 不含指定文字 |
assertContent($value) | 回應內文與指定值一致 |
assertDownload($filename) | 為下載回應 |
JSON
| 方法 | 說明 |
|---|---|
assertJson(array $data) | JSON 含指定資料 |
assertExactJson(array $data) | JSON 與指定資料完全一致 |
assertJsonPath($path, $value) | 指定路徑的值一致 |
assertJsonMissingPath($path) | 指定路徑不存在 |
assertJsonStructure(array $structure) | JSON 具指定結構 |
assertJsonCount($count, $key) | 指定 key 的 JSON 具指定元素數 |
assertJsonFragment(array $data) | JSON 含指定片段 |
assertJsonMissing(array $data) | JSON 不含指定資料 |
assertJsonIsArray() | JSON 為陣列 |
assertJsonIsObject() | JSON 為物件 |
assertJsonValidationErrors($keys) | 含指定 key 的驗證錯誤 |
assertJsonMissingValidationErrors($keys) | 不含指定 key 的驗證錯誤 |
標頭與 Cookie
| 方法 | 說明 |
|---|---|
assertHeader($headerName, $value) | 含指定標頭 |
assertHeaderMissing($headerName) | 不含指定標頭 |
assertCookie($name, $value) | 含指定 cookie |
assertCookieExpired($name) | 指定 cookie 已過期 |
assertCookieMissing($name) | 不含指定 cookie |
assertPlainCookie($name, $value) | 含指定未加密 cookie |
視圖
| 方法 | 說明 |
|---|---|
assertViewIs($value) | 回傳指定視圖 |
assertViewHas($key, $value) | 視圖中含指定資料 |
assertViewHasAll(array $data) | 視圖中含多筆資料 |
assertViewMissing($key) | 視圖中無指定資料 |
驗證
| 方法 | 說明 |
|---|---|
assertValid($keys) | 指定 key 無驗證錯誤 |
assertInvalid($keys) | 指定 key 有驗證錯誤 |
實踐 TDD 的要點
HTTP 測試與 TDD(測試驅動開發)非常契合。留意以下要點會更有效。
從 Feature 測試開始
從 Feature 測試開始
HTTP 測試放在
tests/Feature/ 目錄。從應用程式外部視角先定義行為(請求→回應),能讓待實作功能清晰。使用 RefreshDatabase
使用 RefreshDatabase
需使用資料庫的測試請採用
RefreshDatabase trait。每測試後重置資料庫,可避免測試間資料干擾。維持測試獨立性,能構築不受執行順序影響的穩定測試套件。積極使用工廠
積極使用工廠
使用如
User::factory()->create() 等模型工廠可簡化測試資料建立。定義工廠 state 讓具備特定狀態的模型建立更容易,測試可讀性亦提升。一測試一斷言
一測試一斷言
每個測試方法盡量只驗證一件事,測試失敗時原因更好定位。留意「Arrange(準備)→ Act(執行)→ Assert(驗證)」的 AAA 模式,測試會更易讀。
別忘記認證測試
別忘記認證測試
需認證的路由請務必同時測「已認證」與「未認證」情境,可儘早發現安全問題。
相關頁面
測試入門
瞭解 Laravel 測試的基本寫法與
php artisan test 的使用。