跳转到主要内容

简介

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 中间件会被自动禁用,不需要在测试中显式关闭。

发起请求

在测试中,可以使用 getpostputpatchdelete 等方法发起请求。 这些方法并不会真正发起网络请求,而是在应用内部模拟。 返回值是 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);
    }
}
在发起请求前,可以通过 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()
$this->actingAsGuest();

调试响应

测试中查看响应内容,可以使用 dumpdumpHeadersdumpSession 方法。
<?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();
    }
}
想要在打印后立即停止执行,可以使用 ddddHeadersddBodyddJsonddSession
$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.';
        });
    }
}
若要断言没有抛出异常,可以使用 assertNotReportedassertNothingReported
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 测试的辅助方法。 可以通过 jsongetJsonpostJsonputJsonpatchJsondeleteJsonoptionsJson 等方法发起 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'])
);
可用类型有 stringintegerdoublebooleanarraynull

认证测试

通过 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 的校验错误
方法说明
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 的使用。
最后修改于 2026年7月13日