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('/');
}
}
会话与认证
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() 的第二个参数传入 guard 名,即可以该 guard 进行认证,测试期间该 guard 会成为默认 guard。
$this->actingAs($user, 'web');
若要以未认证状态发起请求,使用 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,
]);
}
}
JSON 响应中的数据也可以以数组变量方式访问。
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 路径断言
assertJsonPath() 可以校验指定路径下的值。
<?php
test('JSON 路径断言', 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');
}
}
若要以指定 guard 认证,可以传入第二个参数。
$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('重复邮箱不能注册', 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');
}
}
会话测试
withSession() 可以在发起请求前预设 Session 数据。
assertSessionHas() 可以断言 Session 中存在某个值。
<?php
test('校验 Session 值', function () {
$response = $this->withSession(['locale' => 'zh'])->get('/');
$response->assertSessionHas('locale', 'zh');
});
<?php
namespace Tests\Feature;
use Tests\TestCase;
class SessionTest extends TestCase
{
public function test_session_value(): void
{
$response = $this->withSession(['locale' => 'zh'])->get('/');
$response->assertSessionHas('locale', 'zh');
}
}
Session 断言一览
| 方法 | 说明 |
|---|
assertSessionHas($key, $value) | Session 中存在指定 key 和 value |
assertSessionHasAll([...]) | Session 中存在多个 key/value |
assertSessionHasErrors($keys) | Session 中存在校验错误 |
assertSessionHasErrorsIn($bag, $keys) | 指定 error bag 中存在错误 |
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(测试驱动开发)非常搭配。留意以下要点会让 TDD 更高效。
HTTP 测试应放在 tests/Feature/ 目录下。先定义从应用外部看到的行为(请求 → 响应),可以让待实现的功能更清晰。
需要用到数据库的测试请使用 RefreshDatabase trait。每个测试后都会重置数据库,防止用例之间的数据干扰。测试的独立性可以让测试套件更稳定,不依赖执行顺序。
使用 User::factory()->create() 这类模型工厂能极大简化数据准备。为常用状态定义 factory state 还能提升测试可读性。
每个测试方法尽量只验证一件事。这样在失败时更容易定位原因。遵循 Arrange(准备)→ Act(执行)→ Assert(断言)的 AAA 结构会更清晰。
对需要认证的路由,务必同时编写“已认证”和“未认证”的测试,以尽早发现安全问题。
相关页面
测试入门
了解 Laravel 中编写测试的基础,以及 php artisan test 的使用。