测试
Livewire 组件很容易测试。底层上它们就是 Laravel 类,因此可以用 Laravel 现有的测试工具来测。此外,Livewire 还提供许多额外工具,让组件测试更加轻松。
本文档将指导你使用推荐的测试框架 Pest 来测试 Livewire 组件;如果你更喜欢 PHPUnit,也可以使用它。
安装 Pest
Pest 是一个注重简洁的优秀 PHP 测试框架,也是在 Livewire 4 中测试 Livewire 组件的推荐方式。
要在 Laravel 应用中安装 Pest,请先移除 PHPUnit(若已安装),再引入 Pest:
composer remove phpunit/phpunit
composer require pestphp/pest --dev --with-all-dependencies接下来,在项目中初始化 Pest:
./vendor/bin/pest --init这会在项目中创建 tests/Pest.php 配置文件。
更详细的安装说明,请参阅 Pest 安装文档。
为基于视图的组件配置 Pest
如果你把测试写在基于视图的组件旁边(单文件或多文件),需要配置 Pest,使其能识别这些测试文件。
首先,更新 tests/Pest.php,把 resources/views 目录包含进去:
pest()->extend(Tests\TestCase::class)
// ...
->in('Feature', '../resources/views');这会告诉 Pest:对 tests/Feature 目录以及 resources/views 中任意位置的测试,都使用你的 TestCase 基类。
接下来,更新 phpunit.xml,为组件测试加入一个测试套件:
<testsuite name="Components">
<directory suffix=".test.php">resources/views</directory>
</testsuite>现在,当你运行 ./vendor/bin/pest 时,Pest 会识别并执行放在组件旁边的测试。
创建第一个测试
你可以在 make:livewire 命令后加上 --test 标志,在组件旁边生成测试文件:
php artisan make:livewire post.create --test对于多文件组件,这会在 resources/views/components/post/create.test.php 创建测试文件:
<?php
use Livewire\Livewire;
it('renders successfully', function () {
Livewire::test('post.create')
->assertStatus(200);
});对于基于类的组件,这会在 tests/Feature/Livewire/Post/CreateTest.php 创建 PHPUnit 测试文件。你可以把它改成 Pest 语法,也可以继续用 PHPUnit——两者与 Livewire 都能很好地配合。
测试页面包含某个组件
你能写的最简单的 Livewire 测试,就是断言某个端点包含并成功渲染了某个 Livewire 组件。
it('component exists on the page', function () {
$this->get('/posts/create')
->assertSeeLivewire('post.create');
});TIP
冒烟测试价值很大
这类测试称为「冒烟测试」——它们确保应用中没有灾难性的问题。虽然简单,但维护成本很低,又能给你一个基础信心:页面能够成功渲染,因此价值巨大。
浏览器测试
Pest v4 内置了由 Playwright 驱动的官方浏览器测试支持。这让你可以在真实浏览器中测试 Livewire 组件,像用户一样与它们交互。
安装浏览器测试
首先,安装 Pest 浏览器插件:
composer require pestphp/pest-plugin-browser --dev接下来,通过 npm 安装 Playwright:
npm install playwright@latest
npx playwright install完整的浏览器测试文档,请参阅 Pest 浏览器测试指南。
编写浏览器测试
除了使用 Livewire::test(),你还可以用 Livewire::visit() 在真实浏览器中测试组件:
it('can create a new post', function () {
Livewire::visit('post.create')
->type('[wire\:model="title"]', 'My first post')
->type('[wire\:model="content"]', 'This is the content')
->press('Save')
->assertSee('Post created successfully');
});浏览器测试比单元测试慢,但能提供端到端的信心:组件在真实浏览器环境中会按预期工作。
可用的浏览器测试断言完整列表,请参阅 Pest 浏览器测试断言。
INFO
何时使用浏览器测试
将浏览器测试用于关键用户流程和复杂交互。对于大多数组件测试,标准的 Livewire::test() 方式更快且足够。
测试视图
Livewire 提供 assertSee(),用于验证文本是否出现在组件的渲染输出中:
use App\Models\Post;
it('displays posts', function () {
Post::factory()->create(['title' => 'My first post']);
Post::factory()->create(['title' => 'My second post']);
Livewire::test('show-posts')
->assertSee('My first post')
->assertSee('My second post');
});断言视图数据
有时测试传入视图的数据,比测试渲染输出更有帮助:
use App\Models\Post;
it('passes all posts to the view', function () {
Post::factory()->count(3)->create();
Livewire::test('show-posts')
->assertViewHas('posts', function ($posts) {
return count($posts) === 3;
});
});对于简单断言,你可以直接传入期望值:
Livewire::test('show-posts')
->assertViewHas('postCount', 3);带身份认证的测试
多数应用需要用户登录。不必在每个测试开头手动认证,使用 actingAs() 方法即可:
use App\Models\User;
use App\Models\Post;
it('user only sees their own posts', function () {
$user = User::factory()
->has(Post::factory()->count(3))
->create();
$stranger = User::factory()
->has(Post::factory()->count(2))
->create();
Livewire::actingAs($user)
->test('show-posts')
->assertViewHas('posts', function ($posts) {
return count($posts) === 3;
});
});测试属性
Livewire 提供用于设置和断言组件属性的工具。
使用 set() 更新属性,使用 assertSet() 验证其值:
it('can set the title property', function () {
Livewire::test('post.create')
->set('title', 'My amazing post')
->assertSet('title', 'My amazing post');
});初始化属性
组件常常会从父组件或路由参数接收数据。把这些数据作为第二个参数传给 Livewire::test():
use App\Models\Post;
it('title field is populated when editing', function () {
$post = Post::factory()->create([
'title' => 'Existing post title',
]);
Livewire::test('post.edit', ['post' => $post])
->assertSet('title', 'Existing post title');
});设置 URL 参数
如果你的组件使用 Livewire 的 URL 功能 在查询字符串中跟踪状态,可用 withQueryParams() 模拟 URL 参数:
use App\Models\Post;
it('can search posts via url query string', function () {
Post::factory()->create(['title' => 'Laravel testing']);
Post::factory()->create(['title' => 'Vue components']);
Livewire::withQueryParams(['search' => 'Laravel'])
->test('search-posts')
->assertSee('Laravel testing')
->assertDontSee('Vue components');
});设置 Cookie
使用 withCookie() 或 withCookies() 为测试设置 cookie:
it('loads discount token from cookie', function () {
Livewire::withCookies(['discountToken' => 'SUMMER2024'])
->test('cart')
->assertSet('discountToken', 'SUMMER2024');
});调用操作
在测试中使用 call() 方法触发组件操作:
use App\Models\Post;
it('can create a post', function () {
expect(Post::count())->toBe(0);
Livewire::test('post.create')
->set('title', 'My new post')
->set('content', 'Post content here')
->call('save');
expect(Post::count())->toBe(1);
});你也可以向操作传递参数:
Livewire::test('post.show')
->call('deletePost', $postId);测试验证
使用 assertHasErrors() 断言已抛出验证错误:
it('title field is required', function () {
Livewire::test('post.create')
->set('title', '')
->call('save')
->assertHasErrors('title');
});测试特定的验证规则:
it('title must be at least 3 characters', function () {
Livewire::test('post.create')
->set('title', 'ab')
->call('save')
->assertHasErrors(['title' => ['min:3']]);
});测试授权
使用 assertUnauthorized() 和 assertForbidden() 确保授权检查按预期工作:
use App\Models\User;
use App\Models\Post;
it('cannot update another users post', function () {
$user = User::factory()->create();
$stranger = User::factory()->create();
$post = Post::factory()->for($stranger)->create();
Livewire::actingAs($user)
->test('post.edit', ['post' => $post])
->set('title', 'Hacked!')
->call('save')
->assertForbidden();
});测试重定向
断言某个操作执行了重定向:
it('redirects to posts index after creating', function () {
Livewire::test('post.create')
->set('title', 'New post')
->set('content', 'Content here')
->call('save')
->assertRedirect('/posts');
});你也可以断言重定向到命名路由或页面组件:
->assertRedirect(route('posts.index'));
->assertRedirectToRoute('posts.index');测试事件
断言组件已派发事件:
it('dispatches event when post is created', function () {
Livewire::test('post.create')
->set('title', 'New post')
->call('save')
->assertDispatched('post-created');
});测试组件之间的事件通信:
it('updates post count when event is dispatched', function () {
$badge = Livewire::test('post-count-badge')
->assertSee('0');
Livewire::test('post.create')
->set('title', 'New post')
->call('save')
->assertDispatched('post-created');
$badge->dispatch('post-created')
->assertSee('1');
});断言事件是以特定参数派发的:
it('dispatches notification when deleting post', function () {
Livewire::test('post.show')
->call('delete', postId: 3)
->assertDispatched('notify', message: 'Post deleted');
});对于复杂断言,可使用闭包:
it('dispatches event with correct data', function () {
Livewire::test('post.show')
->call('delete', postId: 3)
->assertDispatched('notify', function ($event, $params) {
return ($params['message'] ?? '') === 'Post deleted';
});
});测试 JS 求值
断言组件通过 $this->js() 执行了 JavaScript:
it('shows an alert after saving', function () {
Livewire::test('post.create')
->set('title', 'New post')
->call('save')
->assertJs("alert('Post saved!')");
});你也可以断言没有执行任何 JS:
->assertNoJs();使用 PHPUnit
虽然推荐使用 Pest,但你完全可以用 PHPUnit 测试 Livewire 组件。所有相同的测试工具都适用于 PHPUnit 语法。
下面是一个 PHPUnit 示例,便于对照:
<?php
namespace Tests\Feature\Livewire;
use Livewire\Livewire;
use App\Models\Post;
use Tests\TestCase;
class CreatePostTest extends TestCase
{
public function test_can_create_post()
{
$this->assertEquals(0, Post::count());
Livewire::test('post.create')
->set('title', 'My new post')
->set('content', 'Post content')
->call('save');
$this->assertEquals(1, Post::count());
}
public function test_title_is_required()
{
Livewire::test('post.create')
->set('title', '')
->call('save')
->assertHasErrors('title');
}
}本页记录的所有功能在 PHPUnit 中同样适用——只需改用 PHPUnit 的断言语法,而不是 Pest 的。
全部可用的测试方法
以下是你可用的全部 Livewire 测试方法的完整参考:
设置方法
| 方法 | 说明 |
|---|---|
Livewire::test('post.create') | 测试 post.create 组件 |
Livewire::test(UpdatePost::class, ['post' => $post]) | 测试 UpdatePost 组件,并向 mount() 传入参数 |
Livewire::actingAs($user) | 为测试设置已认证用户 |
Livewire::withQueryParams(['search' => '...']) | 设置 URL 查询参数(例如 ?search=...) |
Livewire::withCookie('name', 'value') | 为测试设置一个 cookie |
Livewire::withCookies(['color' => 'blue', 'name' => 'Taylor']) | 设置多个 cookie |
Livewire::withHeaders(['X-Header' => 'value']) | 设置自定义请求头 |
Livewire::withoutLazyLoading() | 在本测试中禁用所有组件的懒加载 |
与组件交互
| 方法 | 说明 |
|---|---|
set('title', '...') | 将 title 属性设为给定值 |
set(['title' => '...', 'content' => '...']) | 使用数组设置多个属性 |
toggle('sortAsc') | 在 true 与 false 之间切换布尔属性 |
call('save') | 调用 save 操作/方法 |
call('remove', $postId) | 调用带参数的方法 |
refresh() | 触发组件重新渲染 |
dispatch('post-created') | 从组件派发事件 |
dispatch('post-created', postId: $post->id) | 派发带参数的事件 |
断言
| 方法 | 说明 |
|---|---|
assertSet('title', '...') | 断言属性等于给定值 |
assertNotSet('title', '...') | 断言属性不等于给定值 |
assertCount('posts', 3) | 断言属性包含 3 个条目 |
assertSee('...') | 断言渲染后的 HTML 包含给定文本 |
assertDontSee('...') | 断言渲染后的 HTML 不包含给定文本 |
assertSeeHtml('<div>...</div>') | 断言渲染输出中存在原始 HTML |
assertDontSeeHtml('<div>...</div>') | 断言渲染输出中不存在原始 HTML |
assertSeeInOrder(['first', 'second']) | 断言字符串按顺序出现在渲染输出中 |
assertDispatched('post-created') | 断言已派发事件 |
assertNotDispatched('post-created') | 断言未派发事件 |
assertHasErrors('title') | 断言某个属性验证失败 |
assertHasErrors(['title' => ['required', 'min:6']]) | 断言特定验证规则失败 |
assertHasNoErrors('title') | 断言某个属性没有验证错误 |
assertRedirect() | 断言已触发重定向 |
assertRedirect('/posts') | 断言重定向到特定 URL |
assertRedirectToRoute('posts.index') | 断言重定向到命名路由 |
assertNoRedirect() | 断言未触发重定向 |
assertViewHas('posts') | 断言数据已传入视图 |
assertViewHas('postCount', 3) | 断言视图数据具有特定值 |
assertViewHas('posts', function ($posts) { ... }) | 断言视图数据通过自定义校验 |
assertViewIs('livewire.show-posts') | 断言渲染了特定视图 |
assertJs("alert('...')") | 断言已执行某个 JS 表达式 |
assertNoJs() | 断言未执行任何 JS |
assertFileDownloaded() | 断言已触发文件下载 |
assertFileDownloaded($filename) | 断言已下载特定文件 |
assertUnauthorized() | 断言抛出了授权异常(401) |
assertForbidden() | 断言访问被禁止(403) |
assertStatus(500) | 断言返回了特定状态码 |