Laravel HTTP 测试

简介

Laravel 提供了便于链式调用的 API,用来向应用发起 HTTP 请求并检查响应。下面是一个功能测试示例:

Pest 版本:

<?php

test('the application returns a successful response', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_the_application_returns_a_successful_response(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

get 方法向应用发起 GET 请求;assertStatus 则断言返回的响应具有指定的 HTTP 状态码。除了这个基本断言,Laravel 还提供了多种断言,用来检查响应头、响应内容、JSON 结构等。

发起请求

在测试中,可以调用 get、post、put、patch 或 delete 方法向应用发起请求。这些方法会在内部模拟整个网络请求过程。

测试请求方法返回 Illuminate\Testing\TestResponse 实例,而不是 Illuminate\Http\Response 实例。TestResponse 提供了多种实用断言,用于检查应用响应:

Pest 版本:

<?php

test('basic request', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_a_basic_request(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

通常,每个测试应只向应用发起一次请求。在同一个测试方法内执行多次请求,可能产生意外行为。

为方便测试,运行测试时会自动禁用 CSRF 中间件。

自定义请求头

可以在请求发送到应用前,使用 withHeaders 方法自定义请求头。该方法允许向请求中添加任意自定义请求头:

Pest 版本:

<?php

test('interacting with headers', function () {
    $response = $this->withHeaders([
        'X-Header' => 'Value',
    ])->post('/user', ['name' => 'Sally']);

    $response->assertStatus(201);
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     */
    public function test_interacting_with_headers(): void
    {
        $response = $this->withHeaders([
            'X-Header' => 'Value',
        ])->post('/user', ['name' => 'Sally']);

        $response->assertStatus(201);
    }
}

发起请求前,可以用 withCookie 或 withCookies 设置 Cookie 值。withCookie 的两个参数分别为 Cookie 名称和值;withCookies 接受名称与值组成的键值对数组:

Pest 版本:

<?php

test('interacting with cookies', function () {
    $response = $this->withCookie('color', 'blue')->get('/');

    $response = $this->withCookies([
        'color' => 'blue',
        'name' => 'Taylor',
    ])->get('/');

    //
});

PHPUnit 版本:

<?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('/');

        //
    }
}

会话与身份认证

Laravel 提供了多种辅助方法,用于在 HTTP 测试中操作会话。首先,可以用 withSession 将会话数据设为指定数组,以便在向应用发起请求前,预先将数据写入会话:

Pest 版本:

<?php

test('interacting with the session', function () {
    $response = $this->withSession(['banned' => false])->get('/');

    //
});

PHPUnit 版本:

<?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('/');

        //
    }
}

Laravel 的会话通常用来维护当前已认证用户的状态。actingAs 辅助方法可以将指定用户作为当前已认证用户。例如,可以使用模型工厂创建并认证一个用户:

Pest 版本:

<?php

use App\Models\User;

test('an action that requires authentication', function () {
    $user = User::factory()->create();

    $response = $this->actingAs($user)
        ->withSession(['banned' => false])
        ->get('/');

    //
});

PHPUnit 版本:

<?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 的第二个参数,指定用于认证该用户的守卫。传入 actingAs 的守卫也会在整个测试期间成为默认守卫:

$this->actingAs($user, 'web');

如果希望确保请求处于未认证状态,可以使用 actingAsGuest:

$this->actingAsGuest();

调试响应

向应用发起测试请求后,可以用 dump、dumpHeaders 和 dumpSession 检查、调试响应内容:

Pest 版本:

<?php

test('basic test', function () {
    $response = $this->get('/');

    $response->dump();
    $response->dumpHeaders();
    $response->dumpSession();
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_basic_test(): void
    {
        $response = $this->get('/');

        $response->dump();
        $response->dumpHeaders();
        $response->dumpSession();
    }
}

也可以使用 dd、ddHeaders、ddBody、ddJson 和 ddSession 输出响应相关信息,然后停止执行:

Pest 版本:

<?php

test('basic test', function () {
    $response = $this->get('/');

    $response->dd();
    $response->ddHeaders();
    $response->ddBody();
    $response->ddJson();
    $response->ddSession();
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_basic_test(): void
    {
        $response = $this->get('/');

        $response->dd();
        $response->ddHeaders();
        $response->ddBody();
        $response->ddJson();
        $response->ddSession();
    }
}

异常处理

有时需要测试应用是否抛出了某个特定异常。可以通过 Exceptions 门面模拟异常处理器,然后使用 assertReported 与 assertNotReported,对请求过程中抛出的异常是否被报告进行断言:

Pest 版本:

<?php

use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Exceptions;

test('exception is thrown', function () {
    Exceptions::fake();

    $response = $this->get('/order/1');

    // Assert an exception was thrown...
    Exceptions::assertReported(InvalidOrderException::class);

    // Assert against the exception...
    Exceptions::assertReported(function (InvalidOrderException $e) {
        return $e->getMessage() === 'The order was invalid.';
    });
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use App\Exceptions\InvalidOrderException;
use Illuminate\Support\Facades\Exceptions;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     */
    public function test_exception_is_thrown(): void
    {
        Exceptions::fake();

        $response = $this->get('/');

        // Assert an exception was thrown...
        Exceptions::assertReported(InvalidOrderException::class);

        // Assert against the exception...
        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('/');

此外,如果希望确认应用没有使用 PHP 语言或所用库已经弃用的功能,可以在请求前调用 withoutDeprecationHandling。禁用弃用警告处理后,弃用警告会转换为异常,从而使测试失败:

$response = $this->withoutDeprecationHandling()->get('/');

assertThrows 可以断言指定闭包中的代码会抛出某种类型的异常:

$this->assertThrows(
    fn () => (new ProcessOrder)->execute(),
    OrderInvalid::class
);

如果希望检查抛出的异常并对它做进一步断言,可以将闭包作为 assertThrows 的第二个参数:

$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 可使用不同的 HTTP 方法发起 JSON 请求,也可以方便地传入数据和请求头。先编写一个测试,向 /api/user 发起 POST 请求,并断言返回了预期 JSON 数据:

Pest 版本:

<?php

test('making an api request', function () {
    $response = $this->postJson('/api/user', ['name' => 'Sally']);

    $response
        ->assertStatus(201)
        ->assertJson([
            'created' => true,
        ]);
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     */
    public function test_making_an_api_request(): void
    {
        $response = $this->postJson('/api/user', ['name' => 'Sally']);

        $response
            ->assertStatus(201)
            ->assertJson([
                'created' => true,
            ]);
    }
}

还可以将响应中的 JSON 数据作为数组元素访问,便于逐项检查返回值:

Pest 版本:

expect($response['created'])->toBeTrue();

PHPUnit 版本:

$this->assertTrue($response['created']);

assertJson 会将响应转换为数组,检查给定数组是否包含在应用返回的 JSON 响应中。因此,只要指定的数据片段存在,即使响应中还包含其他属性,测试仍会通过。

断言 JSON 完全匹配

如前所述,assertJson 用于检查 JSON 响应是否包含某个片段。如果希望验证给定数组与应用返回的 JSON 完全匹配,应使用 assertExactJson:

Pest 版本:

<?php

test('asserting an exact json match', function () {
    $response = $this->postJson('/user', ['name' => 'Sally']);

    $response
        ->assertStatus(201)
        ->assertExactJson([
            'created' => true,
        ]);
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     */
    public function test_asserting_an_exact_json_match(): void
    {
        $response = $this->postJson('/user', ['name' => 'Sally']);

        $response
            ->assertStatus(201)
            ->assertExactJson([
                'created' => true,
            ]);
    }
}

对 JSON 路径进行断言

如果希望验证 JSON 响应的指定路径包含给定数据,应使用 assertJsonPath:

Pest 版本:

<?php

test('asserting a json path value', function () {
    $response = $this->postJson('/user', ['name' => 'Sally']);

    $response
        ->assertStatus(201)
        ->assertJsonPath('team.owner.name', 'Darian');
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     */
    public function test_asserting_a_json_paths_value(): void
    {
        $response = $this->postJson('/user', ['name' => 'Sally']);

        $response
            ->assertStatus(201)
            ->assertJsonPath('team.owner.name', 'Darian');
    }
}

assertJsonPath 也接受闭包,可在闭包中动态判断断言是否应该通过:

$response->assertJsonPath('team.owner.name', fn (string $name) => strlen($name) >= 3);

需要一次断言多个 JSON 路径时,可以使用 assertJsonPaths。每个路径的期望值也可以是闭包:

$response->assertJsonPaths([
    'team.owner.name' => 'Darian',
    'team.owner.email' => fn (string $email) => str($email)->is('*@laravel.com'),
    'team.members.0.name' => 'Sally',
]);

可以用 assertJsonMissingPaths 断言响应中不存在多个指定的 JSON 路径:

$response->assertJsonMissingPaths([
    'team.owner.password',
    'team.members.0.api_token',
]);

流式 JSON 测试

Laravel 提供了链式调用方式来测试应用的 JSON 响应。将闭包传入 assertJson,该闭包会收到一个 Illuminate\Testing\Fluent\AssertableJson 实例,可通过它对应用返回的 JSON 作出断言。where 用于检查某个具体属性,missing 用于检查某个属性不存在:

Pest 版本:

use Illuminate\Testing\Fluent\AssertableJson;

test('fluent 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('victoria@gmail.com'))
                ->whereNot('status', 'pending')
                ->missing('password')
                ->etc()
        );
});

PHPUnit 版本:

use Illuminate\Testing\Fluent\AssertableJson;

/**
 * A basic functional test example.
 */
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('victoria@gmail.com'))
                ->whereNot('status', 'pending')
                ->missing('password')
                ->etc()
        );
}

理解 etc 方法

上例的断言链末尾调用了 etc。这个方法告诉 Laravel,JSON 对象可能还存在其他属性。如果没有调用 etc,而 JSON 对象中包含未被断言检查的其他属性,测试就会失败。

这种行为旨在防止 JSON 响应意外暴露敏感信息:要么显式对属性进行断言,要么通过 etc 显式允许其他属性。

但需要注意,断言链中没有调用 etc,并不代表 JSON 对象内嵌套数组中也一定没有新增属性。这项额外属性检查只作用于调用 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'])
);

hasAny 用来检查指定属性列表中至少有一个属性存在:

$response->assertJson(fn (AssertableJson $json) =>
    $json->has('status')
        ->hasAny('data', 'message', 'code')
);

对 JSON 集合进行断言

路由经常返回包含多个条目的 JSON 响应,例如多个用户:

Route::get('/users', function () {
    return User::all();
});

在这种情况下,可以用流式 JSON 对象的 has 检查响应中的用户。例如,先断言 JSON 响应包含三个用户,再通过 first 对集合中的第一个用户作出断言。first 接受一个闭包;闭包收到针对第一个对象的可断言 JSON 对象,可据此检查该对象:

$response
    ->assertJson(fn (AssertableJson $json) =>
        $json->has(3)
            ->first(fn (AssertableJson $json) =>
                $json->where('id', 1)
                    ->where('name', 'Victoria Faith')
                    ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com'))
                    ->missing('password')
                    ->etc()
            )
    );

如果希望对 JSON 集合中的每个条目作出相同断言,可以使用 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 集合断言的作用范围

有时应用路由会返回一个 JSON 集合,并把它放在某个命名键下:

Route::get('/users', function () {
    return [
        'meta' => [...],
        'users' => User::all(),
    ];
});

测试这些路由时,可以用 has 检查集合中的条目数;也可以用 has 限定后续断言链的作用范围:

$response
    ->assertJson(fn (AssertableJson $json) =>
        $json->has('meta')
            ->has('users', 3)
            ->has('users.0', fn (AssertableJson $json) =>
                $json->where('id', 1)
                    ->where('name', 'Victoria Faith')
                    ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com'))
                    ->missing('password')
                    ->etc()
            )
    );

不必分别调用两次 has 来检查 users 集合,也可以在一次调用中把闭包作为第三个参数。此时闭包会自动执行,且作用范围限定为集合中的第一个条目:

$response
    ->assertJson(fn (AssertableJson $json) =>
        $json->has('meta')
            ->has('users', 3, fn (AssertableJson $json) =>
                $json->where('id', 1)
                    ->where('name', 'Victoria Faith')
                    ->where('email', fn (string $email) => str($email)->is('victoria@gmail.com'))
                    ->missing('password')
                    ->etc()
            )
    );

断言 JSON 类型

如果只需要断言 JSON 响应中的属性具有特定类型,可使用 Illuminate\Testing\Fluent\AssertableJson 的 whereType 和 whereAllType:

$response->assertJson(fn (AssertableJson $json) =>
    $json->whereType('id', 'integer')
        ->whereAllType([
            'users.0.name' => 'string',
            'meta' => 'array'
        ])
);

可以用 | 分隔多个类型,或者将类型数组作为 whereType 的第二个参数。响应值属于列出的任意一个类型,断言就会通过:

$response->assertJson(fn (AssertableJson $json) =>
    $json->whereType('name', 'string|null')
        ->whereType('id', ['string', 'integer'])
);

whereType 与 whereAllType 支持以下类型:string、integer、double、boolean、array 和 null。

测试文件上传

Illuminate\Http\UploadedFile 的 fake 方法可生成供测试使用的模拟文件或图片。与 Storage 门面的 fake 配合使用,可简化文件上传测试。例如,把两者结合起来,就可以测试头像上传表单:

Pest 版本:

<?php

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

test('avatars can be uploaded', function () {
    Storage::fake('avatars');

    $file = UploadedFile::fake()->image('avatar.jpg');

    $response = $this->post('/avatar', [
        'avatar' => $file,
    ]);

    Storage::disk('avatars')->assertExists($file->hashName());
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class ExampleTest 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());
    }
}

如果希望断言某个文件不存在,可以使用 Storage 门面提供的 assertMissing:

Storage::fake('avatars');

// ...

Storage::disk('avatars')->assertMissing('missing.jpg');

自定义模拟文件

使用 UploadedFile 的 fake 创建文件时,可以指定图片宽度、高度和文件大小(单位为 KB),以便更好地测试应用的验证规则:

UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100);

除了图片,还可以用 create 创建其他任意类型的文件:

UploadedFile::fake()->create('document.pdf', $sizeInKilobytes);

需要时可以传入 $mimeType 参数,显式指定文件应返回的 MIME 类型:

UploadedFile::fake()->create(
    'document.pdf', $sizeInKilobytes, 'application/pdf'
);

测试视图

Laravel 允许直接渲染视图,无须向应用发起模拟 HTTP 请求。可在测试中调用 view,传入视图名称和可选的数据数组。该方法返回 Illuminate\Testing\TestView 实例,通过它提供的方法检查视图内容:

Pest 版本:

<?php

test('a welcome view can be rendered', function () {
    $view = $this->view('welcome', ['name' => 'Taylor']);

    $view->assertSee('Taylor');
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_a_welcome_view_can_be_rendered(): void
    {
        $view = $this->view('welcome', ['name' => 'Taylor']);

        $view->assertSee('Taylor');
    }
}

TestView 提供以下断言:assertSee、assertSeeInOrder、assertSeeText、assertSeeTextInOrder、assertDontSee 和 assertDontSeeText。

如果需要获取渲染后的原始视图内容,可以把 TestView 转换成字符串:

$contents = (string) $this->view('welcome');

共享错误信息

有些视图依赖 Laravel 提供的全局错误包中共享的错误。可通过 withViewErrors 向错误包填入错误信息:

$view = $this->withViewErrors([
    'name' => ['Please provide a valid name.']
])->view('form');

$view->assertSee('Please provide a valid name.');

渲染 Blade 与组件

需要时,可以用 blade 对原始 Blade 字符串求值并渲染。与 view 一样,blade 返回 Illuminate\Testing\TestView 实例:

$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');

缓存路由

每次测试运行前,Laravel 都会启动一个新的应用实例,并收集所有已定义的路由。如果应用有很多路由文件,可以向测试用例添加 Illuminate\Foundation\Testing\WithCachedRoutes trait。使用该 trait 的测试会将路由构建一次并保存在内存中,因此整个测试套件只需要执行一次路由收集过程:

Pest 版本:

<?php

use App\Http\Controllers\UserController;
use Illuminate\Foundation\Testing\WithCachedRoutes;

pest()->use(WithCachedRoutes::class);

test('basic example', function () {
    $this->get(action([UserController::class, 'index']));

    // ...
});

PHPUnit 版本:

<?php

namespace Tests\Feature;

use App\Http\Controllers\UserController;
use Illuminate\Foundation\Testing\WithCachedRoutes;
use Tests\TestCase;

class BasicTest extends TestCase
{
    use WithCachedRoutes;

    /**
     * A basic functional test example.
     */
    public function test_basic_example(): void
    {
        $response = $this->get(action([UserController::class, 'index']));

        // ...
    }
}

可用断言

响应断言

Laravel 的 Illuminate\Testing\TestResponse 提供了多种用于应用测试的自定义断言。可以在 json、get、post、put 和 delete 测试方法返回的响应上调用这些断言:

assertAccepted

断言响应的 HTTP 状态码为 202 Accepted:

$response->assertAccepted();

assertBadRequest

断言响应的 HTTP 状态码为 400 Bad Request:

$response->assertBadRequest();

assertClientError

断言响应的 HTTP 状态码属于客户端错误范围,即大于等于 400 且小于 500:

$response->assertClientError();

assertConflict

断言响应的 HTTP 状态码为 409 Conflict:

$response->assertConflict();

assertCookie

断言响应中包含指定 Cookie:

$response->assertCookie($cookieName, $value = null);

assertCookieExpired

断言响应中包含指定 Cookie,且该 Cookie 已经过期:

$response->assertCookieExpired($cookieName);

assertCookieNotExpired

断言响应中包含指定 Cookie,且该 Cookie 尚未过期:

$response->assertCookieNotExpired($cookieName);

assertCookieMissing

断言响应中不包含指定 Cookie:

$response->assertCookieMissing($cookieName);

assertCreated

断言响应的 HTTP 状态码为 201:

$response->assertCreated();

assertDontSee

断言应用返回的响应中不包含指定字符串。除非第二个参数为 false,否则会自动转义该字符串:

$response->assertDontSee($value, $escape = true);

assertDontSeeText

断言响应文本中不包含指定字符串。除非第二个参数为 false,否则会自动转义该字符串。执行断言前,会先将响应内容传入 PHP 的 strip_tags 函数:

$response->assertDontSeeText($value, $escape = true);

assertDownload

断言响应属于“下载”响应。通常表示所调用的路由返回了 Response::download、BinaryFileResponse 或 Storage::download 响应:

$response->assertDownload();

还可以断言可下载文件具有指定的文件名:

$response->assertDownload('image.jpg');

assertExactJson

断言响应与给定 JSON 数据完全匹配:

$response->assertExactJson(array $data);

assertExactJsonStructure

断言响应与给定 JSON 结构完全匹配:

$response->assertExactJsonStructure(array $data);

该方法是 assertJsonStructure 的更严格版本。响应包含任何未在预期 JSON 结构中显式列出的键时,assertExactJsonStructure 就会失败。

assertFailedDependency

断言响应的 HTTP 状态码为 424 Failed Dependency:

$response->assertFailedDependency();

assertForbidden

断言响应的 HTTP 状态码为 403 Forbidden:

$response->assertForbidden();

assertFound

断言响应的 HTTP 状态码为 302 Found:

$response->assertFound();

assertGone

断言响应的 HTTP 状态码为 410 Gone:

$response->assertGone();

assertHeader

断言响应包含指定响应头及其值:

$response->assertHeader($headerName, $value = null);

assertHeaderContains

断言指定响应头的值包含给定子字符串:

$response->assertHeaderContains($headerName, $value);

assertHeaderMissing

断言响应中不存在指定响应头:

$response->assertHeaderMissing($headerName);

assertInternalServerError

断言响应的 HTTP 状态码为 500 Internal Server Error:

$response->assertInternalServerError();

assertJson

断言响应包含给定 JSON 数据:

$response->assertJson(array $data, $strict = false);

assertJson 会将响应转换为数组,检查给定数组是否包含在应用返回的 JSON 响应中。因此,只要指定数据片段存在,即使 JSON 响应还包含其他属性,测试仍会通过。

assertJsonCount

断言响应 JSON 中指定键对应的数组具有预期的条目数:

$response->assertJsonCount($count, $key = null);

assertJsonFragment

断言响应中的任意位置包含给定 JSON 数据:

Route::get('/users', function () {
    return [
        'users' => [
            [
                'name' => 'Taylor Otwell',
            ],
        ],
    ];
});

$response->assertJsonFragment(['name' => 'Taylor Otwell']);

assertJsonIsArray

断言响应的 JSON 是数组:

$response->assertJsonIsArray();

assertJsonIsObject

断言响应的 JSON 是对象:

$response->assertJsonIsObject();

assertJsonMissing

断言响应不包含给定 JSON 数据:

$response->assertJsonMissing(array $data);

assertJsonMissingExact

断言响应不包含完全匹配给定内容的 JSON 数据:

$response->assertJsonMissingExact(array $data);

assertJsonMissingValidationErrors

断言响应中指定键没有 JSON 验证错误:

$response->assertJsonMissingValidationErrors($keys);

更通用的 assertValid 可以同时检查:JSON 响应中没有验证错误,并且会话中也没有暂存错误。

assertJsonPath

断言响应的指定路径包含给定数据:

$response->assertJsonPath($path, $expectedValue);

例如,应用返回以下 JSON 响应:

{
    "user": {
        "name": "Steve Schoger"
    }
}

可以按如下方式断言 user 对象的 name 属性与指定值相同:

$response->assertJsonPath('user.name', 'Steve Schoger');

assertJsonPaths

断言响应的多个指定路径包含给定数据:

$response->assertJsonPaths(array $paths);

例如,可以一次检查响应中的多个值:

$response->assertJsonPaths([
    'user.name' => 'Steve Schoger',
    'user.email' => fn (string $email) => str($email)->endsWith('@laravel.com'),
]);

assertJsonMissingPath

断言响应中不存在指定路径:

$response->assertJsonMissingPath($path);

例如,应用返回以下 JSON 响应:

{
    "user": {
        "name": "Steve Schoger"
    }
}

可以断言 user 对象中不包含 email 属性:

$response->assertJsonMissingPath('user.email');

assertJsonMissingPaths

断言响应中不存在多个指定路径:

$response->assertJsonMissingPaths($paths);

例如,可以一次检查响应中缺少多个路径:

$response->assertJsonMissingPaths([
    'user.email',
    'user.password',
]);

assertJsonStructure

断言响应具有指定的 JSON 结构:

$response->assertJsonStructure(array $structure);

例如,应用返回的 JSON 响应包含以下数据:

{
    "user": {
        "name": "Steve Schoger"
    }
}

可以按如下方式断言 JSON 结构符合预期:

$response->assertJsonStructure([
    'user' => [
        'name',
    ]
]);

有时,应用返回的 JSON 响应会包含对象数组:

{
    "user": [
        {
            "name": "Steve Schoger",
            "age": 55,
            "location": "Earth"
        },
        {
            "name": "Mary Schoger",
            "age": 60,
            "location": "Earth"
        }
    ]
}

此时可以使用 *,对数组中所有对象的结构进行断言:

$response->assertJsonStructure([
    'user' => [
        '*' => [
             'name',
             'age',
             'location'
        ]
    ]
]);

assertJsonValidationErrors

断言响应在指定键下包含给定 JSON 验证错误。该方法适合验证错误通过 JSON 结构返回的响应;如果错误被暂存到会话中,应使用相应的会话断言:

$response->assertJsonValidationErrors(array $data, $responseKey = 'errors');

更通用的 assertInvalid 可检查响应中存在以 JSON 返回的验证错误,或存在暂存到会话的错误。

assertJsonValidationErrorFor

断言响应中指定键具有 JSON 验证错误:

$response->assertJsonValidationErrorFor(string $key, $responseKey = 'errors');

assertMethodNotAllowed

断言响应的 HTTP 状态码为 405 Method Not Allowed:

$response->assertMethodNotAllowed();

assertMovedPermanently

断言响应的 HTTP 状态码为 301 Moved Permanently:

$response->assertMovedPermanently();

assertLocation

断言响应的 Location 响应头具有指定 URI 值:

$response->assertLocation($uri);

assertContent

断言给定字符串与响应内容相同:

$response->assertContent($value);

assertNoContent

断言响应具有指定 HTTP 状态码,且没有内容:

$response->assertNoContent($status = 204);

assertStreamed

断言响应是流式响应:

$response->assertStreamed();

assertStreamedContent

断言给定字符串与流式响应的内容相同:

$response->assertStreamedContent($value);

assertNotFound

断言响应的 HTTP 状态码为 404 Not Found:

$response->assertNotFound();

assertOk

断言响应的 HTTP 状态码为 200:

$response->assertOk();

assertPaymentRequired

断言响应的 HTTP 状态码为 402 Payment Required:

$response->assertPaymentRequired();

assertPlainCookie

断言响应包含指定的未加密 Cookie:

$response->assertPlainCookie($cookieName, $value = null);

assertRedirect

断言响应重定向到指定 URI:

$response->assertRedirect($uri = null);

assertRedirectBack

断言响应重定向回前一页面:

$response->assertRedirectBack();

assertRedirectBackWithErrors

断言响应重定向回前一页面,且会话包含指定错误:

$response->assertRedirectBackWithErrors(
    array $keys = [], $format = null, $errorBag = 'default'
);

assertRedirectBackWithoutErrors

断言响应重定向回前一页面,且会话不包含任何错误信息:

$response->assertRedirectBackWithoutErrors();

assertRedirectContains

断言响应重定向到的 URI 包含指定字符串:

$response->assertRedirectContains($string);

assertRedirectToRoute

断言响应重定向到指定的命名路由:

$response->assertRedirectToRoute($name, $parameters = []);

assertRedirectToSignedRoute

断言响应重定向到指定的签名路由:

$response->assertRedirectToSignedRoute($name = null, $parameters = []);

assertRequestTimeout

断言响应的 HTTP 状态码为 408 Request Timeout:

$response->assertRequestTimeout();

assertSee

断言响应包含指定字符串。除非第二个参数为 false,否则会自动转义该字符串:

$response->assertSee($value, $escape = true);

assertSeeInOrder

断言响应按给定顺序包含指定字符串。除非第二个参数为 false,否则会自动转义这些字符串:

$response->assertSeeInOrder(array $values, $escape = true);

assertSeeText

断言响应文本包含指定字符串。除非第二个参数为 false,否则会自动转义该字符串。执行断言前,会先将响应内容传入 PHP 的 strip_tags 函数:

$response->assertSeeText($value, $escape = true);

assertSeeTextInOrder

断言响应文本按给定顺序包含指定字符串。除非第二个参数为 false,否则会自动转义这些字符串。执行断言前,会先将响应内容传入 PHP 的 strip_tags 函数:

$response->assertSeeTextInOrder(array $values, $escape = true);

assertServerError

断言响应的 HTTP 状态码属于服务器错误范围,即大于等于 500 且小于 600:

$response->assertServerError();

assertServiceUnavailable

断言响应的 HTTP 状态码为 503 Service Unavailable:

$response->assertServiceUnavailable();

assertSessionHas

断言会话包含指定数据:

$response->assertSessionHas($key, $value = null);

需要时,可把闭包作为 assertSessionHas 的第二个参数。如果闭包返回 true,断言即通过:

$response->assertSessionHas($key, function (User $value) {
    return $value->name === 'Taylor Otwell';
});

assertSessionHasInput

断言会话的暂存输入数组中存在指定值:

$response->assertSessionHasInput($key, $value = null);

需要时,可把闭包作为 assertSessionHasInput 的第二个参数。如果闭包返回 true,断言即通过:

use Illuminate\Support\Facades\Crypt;

$response->assertSessionHasInput($key, function (string $value) {
    return Crypt::decryptString($value) === 'secret';
});

assertSessionHasAll

断言会话包含给定数组中的所有键值对:

$response->assertSessionHasAll(array $data);

例如,会话中包含 name 与 status 键时,可以按如下方式断言它们都存在且具有指定值:

$response->assertSessionHasAll([
    'name' => 'Taylor Otwell',
    'status' => 'active',
]);

assertSessionHasErrors

断言会话包含指定 $keys 对应的错误。如果 $keys 是关联数组,则检查每个字段(键)都具有指定错误信息(值)。该方法适用于将验证错误暂存到会话的路由,而不是以 JSON 结构返回错误的路由:

$response->assertSessionHasErrors(
    array $keys = [], $format = null, $errorBag = 'default'
);

例如,要断言 name 与 email 字段的验证错误信息已暂存到会话,可以这样调用 assertSessionHasErrors:

$response->assertSessionHasErrors(['name', 'email']);

也可以断言指定字段具有某个特定验证错误信息:

$response->assertSessionHasErrors([
    'name' => 'The given name was invalid.'
]);

更通用的 assertInvalid 可检查响应中存在以 JSON 返回的验证错误,或存在暂存到会话的错误。

assertSessionHasErrorsIn

断言指定错误包中存在 $keys 对应的会话错误。如果 $keys 是关联数组,则检查该错误包中每个字段(键)具有指定错误信息(值):

$response->assertSessionHasErrorsIn($errorBag, $keys = [], $format = null);

assertSessionHasNoErrors

断言会话中没有验证错误:

$response->assertSessionHasNoErrors();

assertSessionDoesntHaveErrors

断言会话中指定键没有验证错误:

$response->assertSessionDoesntHaveErrors($keys = [], $format = null, $errorBag = 'default');

更通用的 assertValid 可以同时检查:JSON 响应中没有验证错误,并且会话中也没有暂存错误。

assertSessionMissing

断言会话中不存在指定键:

$response->assertSessionMissing($key);

assertSessionMissingInput

断言会话的暂存输入数组中不存在指定输入键:

$response->assertSessionMissingInput($key);

assertStatus

断言响应具有指定 HTTP 状态码:

$response->assertStatus($code);

assertSuccessful

断言响应的 HTTP 状态码表示成功,即大于等于 200 且小于 300:

$response->assertSuccessful();

assertTooManyRequests

断言响应的 HTTP 状态码为 429 Too Many Requests:

$response->assertTooManyRequests();

assertUnauthorized

断言响应的 HTTP 状态码为 401 Unauthorized:

$response->assertUnauthorized();

assertUnprocessable

断言响应的 HTTP 状态码为 422 Unprocessable Entity:

$response->assertUnprocessable();

assertUnsupportedMediaType

断言响应的 HTTP 状态码为 415 Unsupported Media Type:

$response->assertUnsupportedMediaType();

assertValid

断言响应中指定键没有验证错误。无论验证错误以 JSON 结构返回,还是被暂存到会话,都可以用该方法检查:

// Assert that no validation errors are present...
$response->assertValid();

// Assert that the given keys do not have validation errors...
$response->assertValid(['name', 'email']);

assertInvalid

断言响应中指定键具有验证错误。无论验证错误以 JSON 结构返回,还是被暂存到会话,都可以用该方法检查:

$response->assertInvalid(['name', 'email']);

还可以断言指定键具有某个特定验证错误信息。既可以提供完整信息,也可以只提供其中一小段:

$response->assertInvalid([
    'name' => 'The name field is required.',
    'email' => 'valid email address',
]);

如果希望断言只有指定字段具有验证错误,可以使用 assertOnlyInvalid:

$response->assertOnlyInvalid(['name', 'email']);

assertViewHas

断言响应视图中包含指定数据:

$response->assertViewHas($key, $value = null);

把闭包作为 assertViewHas 的第二个参数,就可以检查指定视图数据并作出断言:

$response->assertViewHas('user', function (User $user) {
    return $user->name === 'Taylor';
});

还可以将视图数据作为响应中的数组元素访问,便于检查:

Pest 版本:

expect($response['name'])->toBe('Taylor');

PHPUnit 版本:

$this->assertEquals('Taylor', $response['name']);

assertViewHasAll

断言响应视图包含指定的数据列表:

$response->assertViewHasAll(array $data);

可以用该方法检查视图中存在与指定键对应的数据:

$response->assertViewHasAll([
    'name',
    'email',
]);

也可以同时检查视图数据存在且具有指定值:

$response->assertViewHasAll([
    'name' => 'Taylor Otwell',
    'email' => 'taylor@example.com,',
]);

assertViewIs

断言路由返回的是指定视图:

$response->assertViewIs($value);

assertViewMissing

断言应用响应中的视图未获得指定数据键:

$response->assertViewMissing($key);

身份认证断言

Laravel 提供了多种身份认证相关断言,可用于应用的功能测试。注意,这些方法应在测试类本身上调用,而不是在 get、post 等方法返回的 Illuminate\Testing\TestResponse 实例上调用。

assertAuthenticated

断言存在已认证用户:

$this->assertAuthenticated($guard = null);

assertGuest

断言用户未通过认证:

$this->assertGuest($guard = null);

assertAuthenticatedAs

断言指定用户已通过认证:

$this->assertAuthenticatedAs($user, $guard = null);

验证断言

Laravel 提供两种主要的验证相关断言,用于确认请求中提供的数据是否有效。

assertValid

断言响应中指定键没有验证错误。无论验证错误以 JSON 结构返回,还是被暂存到会话,都可以用该方法检查:

// Assert that no validation errors are present...
$response->assertValid();

// Assert that the given keys do not have validation errors...
$response->assertValid(['name', 'email']);

assertInvalid

断言响应中指定键具有验证错误。无论验证错误以 JSON 结构返回,还是被暂存到会话,都可以用该方法检查:

$response->assertInvalid(['name', 'email']);

还可以断言指定键具有某个特定验证错误信息。既可以提供完整信息,也可以只提供其中一小段:

$response->assertInvalid([
    'name' => 'The name field is required.',
    'email' => 'valid email address',
]);

作者:Taylor Otwell 与 Laravel 文档贡献者。来源:HTTP Tests,实际核对 Laravel 13.x 官方文档及文档仓库,核对日期 2026-10-03。

本文按 Laravel 文档仓库的 MIT 许可证使用。全部正文译为中文,保留代码与原注释;将选项卡示例分为 Pest 与 PHPUnit 两组,把文档内链改为 Laravel 13.x 官方链接,将断言索引改排为列表。异常报告断言的原文措辞作了明确澄清。本任务未执行这些测试;示例需要对应应用的路由、模型、工厂及隔离测试环境。

MIT 许可证全文

The MIT License (MIT)

Copyright (c) Taylor Otwell

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容