HTTP 测试
简介
Laravel 提供了一个非常流畅的 API,用于向应用程序发出 HTTP 请求并检查响应。例如,看看下面定义的特性测试:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* 基本功能测试示例。
*/
public function test_a_basic_request(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
get
方法向应用程序发出 Get
请求,而 assertStatus
方法则断言返回的响应应该具有给定的 HTTP 状态代码。除了这个简单的断言之外,Laravel 还包含各种用于检查响应头、内容、JSON 结构等的断言。
创建请求
要向应用程序发出请求,可以在测试中调用get
、post
、put
、patch
或delete
方法。这些方法实际上不会向应用程序发出「真正的」HTTP 请求。相反,整个网络请求是在内部模拟的。
测试请求方法不返回Illuminate\Http\Response
实例,而是返回Illuminate\Testing\TestResponse
实例,该实例提供各种有用的断言,允许你检查应用程序的响应:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* 基本功能测试示例。
*/
public function test_a_basic_request(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
通常,你的每个测试应该只向你的应用发出一个请求。如果在单个测试方法中执行多个请求,则可能会出现意外行为。
为了方便起见,运行测试时会自动禁用 CSRF 中间件。
自定义请求头
你可以使用此 withHeaders
方法自定义请求的标头,然后再将其发送到应用程序。这使你可以将任何想要的自定义标头添加到请求中:
<?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);
}
}
Cookies
在发送请求前你可以使用 withCookie
或 withCookies
方法设置 cookie。withCookie
接受 cookie 的名称和值这两个参数,而 withCookies
方法接受一个名称 / 值对数组:
<?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) / 认证 (Authentication)
Laravel 提供了几个可在 HTTP 测试时使用 Session 的辅助函数。首先,你需要传递一个数组给 withSession
方法来设置 session 数据。这样在应用程序的测试请求发送之前,就会先去给数据加载 session:
<?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 的 session 通常用于维护当前已验证用户的状态。因此,actingAs
方法提供了一种将给定用户作为当前用户进行身份验证的便捷方法。例如,我们可以使用一个工厂模式来生成和认证一个用户:
<?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')
调试响应
在向你的应用程序发出测试请求之后,可以使用 dump
、dumpHeaders
和 dumpSession
方法来检查和调试响应内容:
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* 基本功能测试示例。
*/
public function test_basic_test(): void
{
$response = $this->get('/');
$response->dumpHeaders();
$response->dumpSession();
$response->dump();
}
}
或者,你可以使用 dd
、ddHeaders
和 ddSession
方法转储有关响应的信息,然后停止执行:
<?php
namespace Tests\Feature;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* 基本功能测试示例。
*/
public function test_basic_test(): void
{
$response = $this->get('/');
$response->ddHeaders();
$response->ddSession();
$response->dd();
}
}
异常处理
有时你可能想要测试你的应用程序是否引发了特定异常。为了确保异常不会被 Laravel 的异常处理程序捕获并作为 HTTP 响应返回,可以在发出请求之前调用 withoutExceptionHandling
方法:
$response = $this->withoutExceptionHandling()->get('/');
此外,如果想确保你的应用程序没有使用 PHP 语言或你的应用程序正在使用的库已弃用的功能,你可以在发出请求之前调用 withoutDeprecationHandling
方法。禁用弃用处理时,弃用警告将转换为异常,从而导致你的测试失败:
$response = $this->withoutDeprecationHandling()->get('/');
测试 JSON APIs
Laravel 也提供了几个辅助函数来测试 JSON APIs 和其响应。例如,json
、getJson
、postJson
、putJson
、patchJson
、deleteJson
以及 optionsJson
可以被用于发送各种 HTTP 动作。你也可以轻松地将数据和请求头传递到这些方法中。首先,让我们实现一个测试示例,发送 POST
请求到 /api/user
,并断言返回的期望数据:
<?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 响应数据可以作为响应上的数组变量进行访问,从而使你可以方便地检查 JSON 响应中返回的各个值:
$this->assertTrue($response['created']);
assertJson
方法将响应转换为数组,并利用 PHPUnit::assertArraySubset
验证给定数组是否存在于应用程序返回的 JSON 响应中。因此,如果 JSON 响应中还有其他属性,则只要存在给定的片段,此测试仍将通过。
验证 JSON 完全匹配
如前所述,assertJson
方法可用于断言 JSON 响应中存在 JSON 片段。如果你想验证给定数组是否与应用程序返回的 JSON 完全匹配,则应使用 assertExactJson
方法:
<?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 路径
如果你想验证 JSON 响应是否包含指定路径上的某些给定数据,可以使用 assertJsonPath
方法:
<?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');
}
}
assertJsonPath
方法也接受一个闭包,可以用来动态地确定断言是否应该通过。
$response->assertJsonPath('team.owner.name', fn (string $name) => strlen($name) >= 3);
JSON 流式测试
Laravel 还提供了一种漂亮的方式来流畅地测试应用程序的 JSON 响应。首先,将闭包传递给 assertJson
方法。这个闭包将使用 Illuminate\Testing\Fluent\AssertableJson
的实例调用,该实例可用于对应用程序返回的 JSON 进行断言。 where
方法可用于对 JSON 的特定属性进行断言,而 missing
方法可用于断言 JSON 中缺少特定属性:
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('victoria@gmail.com'))
->whereNot('status', 'pending')
->missing('password')
->etc()
);
}
了解 etc
方法
在上面的例子中, 你可能已经注意到我们在断言链的末端调用了 etc
方法. 这个方法通知Laravel,在JSON对象上可能还有其他的属性存在。如果没有使用 etc
方法, 如果你没有对JSON对象的其他属性进行断言, 测试将失败.
这种行为背后的意图是保护你不会在你的 JSON 响应中无意地暴露敏感信息,因为它迫使你明确地对该属性进行断言或通过 etc
方法明确地允许额外的属性。
然而,你应该知道,在你的断言链中不包括 etc
方法并不能确保额外的属性不会被添加到嵌套在 JSON 对象中的数组。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'])
);
你可以使用 hasAny
方法来确定是否存在给定属性列表中的至少一个:
$response->assertJson(fn (AssertableJson $json) =>
$json->has('status')
->hasAny('data', 'message', 'code')
);
断言反对 JSON 集合
通常,你的路由将返回一个 JSON 响应,其中包含多个项目,例如多个用户:
Route::get('/users', function () {
return User::all();
});
在这些情况下,我们可以使用 fluent JSON 对象的 has
方法对响应中包含的用户进行断言。例如,让我们断言 JSON 响应包含三个用户。接下来,我们将使用 first
方法对集合中的第一个用户进行一些断言。 first
方法接受一个闭包,该闭包接收另一个可断言的 JSON 字符串,我们可以使用它来对 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 集合范围断言
有时,你的应用程序的路由将返回分配有命名键的 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
facade 的 fake
方法相结合,大幅度简化了文件上传测试。举个例子,你可以结合这两者的功能非常方便地进行头像上传表单测试:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
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
facade 提供的 AssertMissing
方法:
Storage::fake('avatars');
// ...
Storage::disk('avatars')->assertMissing('missing.jpg');
虚拟文件定制
当使用 UploadedFile
类提供的 fake
方法创建文件时,你可以指定图片的宽度、高度和大小(以千字节为单位),以便更好地测试你的应用程序的验证规则。
UploadedFile::fake()->image('avatar.jpg', $width, $height)->size(100);