使用 Laravel Dusk 测试 Vue 组件

发布时间 作者

Testing Vue components with Laravel Dusk image

为项目添加测试总是有利于不同的方面,但选择正确的策略对于许多开发人员来说可能是一场斗争。

当您使用不同的工具或框架时,问题会加剧,虽然“尽可能多地进行测试”在理论上听起来是个好主意,但在实践中可能大相径庭。

以下来自 Twitter 团队的一篇有趣的文章,介绍了他们对 功能测试的看法

Taylor Otwell 在他的 双周 Laravel 提示时事通讯 中分享了 Twitter 的文章,如果您还没有订阅,请订阅。

让我们使用 Vue.js 和 Laravel 构建一个简单的待办事项列表,以说明如何使用 Laravel dusk 添加浏览器测试。

如何开始?

我们可以开始构建一个小型 API 来处理 task 资源的 CRUD 操作。

namespace App\Http\Controllers;
 
use Carbon\Carbon;
use App\Models\Task;
use Illuminate\Http\Request;
 
class TaskController extends Controller
{
public function store(Request $request)
{
$request->validate([
'text' => 'required'
]);
 
return Task::create([
'text' => $request->text,
'user_id' => auth()->user()->id,
'is_completed' => false
]);
}
 
public function destroy(Task $task)
{
$task->delete();
 
return response()->json(['message' => 'Task deleted'], 200);
}
 
public function update(Task $task)
{
return tap($task)->update(request()->only(['is_completed', 'text']))->fresh();
}
}

此控制器中使用了一些概念

现在,让我们将路由添加到 routes/api.php 文件中,以处理每个操作。

Route::resource('task', 'TaskController')->only('store', 'destroy', 'update');

任务迁移文件如下所示

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
 
class CreateTasksTable extends Migration
{
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->increments('id');
$table->text('text');
$table->timestamp('is_completed')->boolean()->default(false);
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users');
$table->timestamps();
});
}
 
public function down()
{
Schema::dropIfExists('tasks');
}
}

端点测试

在这种情况下,我们使用了 laravel 为我们提供的许多开箱即用的功能,因此,无需(暂时)单独测试任何内容。相反,我们可以使用“端点测试”,这更有意义,因为我们正在尝试构建一个 API。

namespace Tests\Feature\API;
 
use Tests\TestCase;
use App\Models\User;
use App\Models\Task;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
 
class TasksTest extends TestCase
{
use DatabaseMigrations;
 
/** @test */
public function user_can_create_tasks()
{
$user = factory(User::class)->create();
$task = [
'text' => 'New task text',
'user_id' => $user->id
];
 
$response = $this->actingAs($user)->json('POST', 'api/task', $task);
 
$response->assertStatus(201);
$this->assertDatabaseHas('tasks', $task);
}
 
/** @test */
public function guest_users_can_not_create_tasks()
{
$task = [
'text' => 'new text',
'user_id' => 1
];
 
$response = $this->json('POST', 'api/task', $task);
 
$response->assertstatus(401);
$this->assertDatabaseMissing('tasks', $task);
}
 
/** @test */
public function user_can_delete_tasks()
{
$user = factory(User::class)->create();
$task = factory(Task::class)->create([
'text' => 'task to delete',
'user_id' => $user->id
]);
 
$response = $this->actingAs($user)->json('DELETE', "api/task/$task->id");
 
$response->assertstatus(200);
$this->assertDatabaseMissing('tasks', ['id' => $task->id]);
}
 
/** @test */
public function user_can_complete_tasks()
{
$user = factory(User::class)->create();
$task = factory(Task::class)->create([
'text' => 'task to complete',
'user_id' => $user->id
]);
 
Passport::actingAs($user);
$response = $this->json('PUT', "api/task/$task->id", ['is_completed' => true]);
 
$response->assertstatus(200);
$this->assertNotNull($task->fresh()->is_completed);
}
}

您需要使用 artisan 创建 TaskFactory::class

$ php artisan make:factory TaskFactory

它看起来像这样

use Faker\Generator as Faker;
use Carbon\Carbon;
 
$factory->define(App\Models\Task::class, function (Faker $faker) {
return [
'text' => $faker->sentence(6),
'is_completed' => null
];
});

此时,您应该能够运行测试套件并获得正面的响应

$ phpunit
 
PHPUnit 6.2.4 by Sebastian Bergmann and contributors.
 
.... 4 / 4 (100%)
 
Time: 328 ms, Memory: 24.00MB
 
OK (4 tests, 8 assertions)

使用 Vue 创建一个新的任务组件

我们可以在此处使用单文件组件。为了便于阅读,我将代码分成两个不同的块,但请注意它们都属于同一个文件。

TasksComponent.vue 文件

<template>
<div class="w-full sm:w-1/2 lg:w-1/3 rounded shadow">
<h2 class="bg-yellow-dark text-sm py-2 px-4 font-hairline font-mono text-yellow-darker">Tasks</h2>
<ul class="list-reset px-4 py-4 font-serif bg-yellow-light h-48 overflow-y-scroll scrolling-touch">
<li v-for="(task, index) in tasks" class="flex">
<label class="flex w-5/6 flex-start py-1 block text-grey-darkest font-bold cursor-pointer">
<input
class="mr-2 cursor-pointer"
type="checkbox"
:dusk="`check-task${task.id}`"
:checked="checked(task)"
@click="completeTask(task)"
>
<span :class="[{'line-through' : task.is_completed}, 'text-sm italic font-normal']">
{{ task.text }}
</span>
</label>
<span
class="flex-1 cursor-pointer text-center rounded-full px-3 text-yellow-light hover:text-yellow-darker text-xs py-1"
@click="removeTask(index, task)"
:dusk="`remove-task${task.id}`"
>✖</span>
</li>
</ul>
<form class="w-full text-sm" @submit.prevent="createTask">
<div class="flex items-center bg-yellow-lighter py-2">
<input class="appearance-none bg-transparent border-none w-3/4 text-yellow-darkest mr-3 py-1 px-2 font-serif italic"
type="text"
placeholder="New Task"
aria-label="New Task"
v-model="newTask"
dusk="task-input"
>
<button
class="flex-no-shrink bg-yellow hover:bg-yellow font-base font-normal text-yellow-darker py-2 px-4 rounded"
type="button"
dusk="task-submit"
@click="createTask"
>
Add
</button>
</div>
</form>
</div>
</template>

我正在使用 tailwindcss 框架,等一下看结果。

dusk="" 属性充当自定义选择器,供 laravel dusk 用来与页面上的 HTML 元素进行交互。

脚本部分

export default {
props: ['initial-tasks'],
data() {
return {
newTask: '',
tasks: this.initialTasks
}
},
methods: {
createTask(event) {
if (this.newTask.trim().length === 0) {
return;
}
axios.post('/api/task', {
text: this.newTask
}).then((response) =&gt; {
this.tasks.push(response.data);
this.newTask = '';
}).catch((e) =&gt; console.error(e));
},
completeTask(task) {
let status = ! task.is_completed;
axios.put(\`/api/task/${task.id}\`, {
is_completed: status
}).then((response) =&gt; {
task.is_completed = response.data.is_completed
}).catch((e) =&gt; console.error(e));
},
checked(task) {
return task.is_completed;
},
removeTask(index, task) {
axios.delete(\`/api/task/${task.id}\`)
.then((response) =&gt; {
this.tasks = [
...this.tasks.slice(0, index),
...this.tasks.slice(index + 1)
];
}).catch((e) =&gt; console.error(e));
}
}
}

主页

您可以在 views/ 目录中创建一个名为 home.blade.php 的文件,其中包含以下代码

@extends('layouts.app')
 
@section('body')
<div class="container px-4 sm:px-0 mx-auto py-8">
 
</div>
@endsection

此页面扩展了一个基本布局视图

<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<title>{{ config('app.name', 'Laravel') }}</title>
</head>
<body class="font-sans antialiased text-black leading-tight">
<div id="app">
@yield('body')
</div>
<a href="http://%20mix('js/app.js')%20">http://%20mix('js/app.js')%20</a>
</body>
</html>

不要忘记创建一个路由来返回此视图

Route::get('/', function () {
$tasks = ;
return view('home', [
'tasks' => auth()->user()->tasks->all()
]);
});

编译资源

我们可以使用 larave mix 轻松地编译所有资源

let mix = require('laravel-mix')
require('laravel-mix-purgecss')
 
mix.js('resources/assets/js/app.js', 'public/js')
.postCss('resources/assets/css/app.css', 'public/css')
.options({
postCss: [
require('postcss-import')(),
require('tailwindcss')(),
require('postcss-cssnext')({
// Mix adds autoprefixer already, don't need to run it twice
features: { autoprefixer: false }
}),
]
})
.purgeCss();

不要忘记运行 npm 任务来编译资源

$ npm run dev

测试组件

这里是一切变得有趣的地方。我们如何才能确保一切正常?

嗯,我们已经为 API 准备了一些测试,但这在这种情况下不太有用,因为我们可能会在主页上出现 500 错误,而这些测试将通过。

我们不能仅仅使用 phpunit 来测试此组件,因为测试依赖于浏览器和 JavaScript 来测试异步代码。

页面加载后,Vue 将在主页上呈现 <template></template>

在这种情况下,浏览器测试似乎是正确的选择。

使用 Laravel Dusk 进行浏览器测试

Laravel Dusk 提供了一个富有表现力、易于使用的浏览器自动化和测试 API

第一步应该是安装 Larave dusk。您可以按照 官方文档 中的说明进行操作。

如果您需要在浏览器测试中使用数据库与 dusk 结合使用,请不要使用内存中数据库。

https://twitter.com/StephCoinon/status/962862247612768256

创建一个新的浏览器测试文件

您可以使用 artisan 执行此操作

$ php artisan dusk:make TasksTest

现在添加逻辑来测试 task CRUD,模拟新用户在页面上的交互

namespace Tests\Browser;
 
use App\Models\User;
use App\Models\Task;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
use Laravel\Passport\Passport;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
 
class DashboardTest extends DuskTestCase
{
use DatabaseMigrations;
 
protected $user;
 
protected function setUp()
{
parent::setUp();
$this->user = factory(User::class)->create();
}
 
/** @test */
public function create_tasks()
{
$this->browse(function (Browser $browser) {
$browser->loginAs($this->user)
->visit('/')
->assertSee('Tasks');
 
$browser
->waitForText('Tasks')
->type('@task-input', 'first task')
->click('@task-submit')
->waitForText('first task')
->assertSee('first task');
 
$browser->type('@task-input', 'second task')
->press('@task-submit')
->waitForText('second task')
->assertSee('second task');
 
$this->assertDatabaseHas('tasks', ['text' => 'first task']);
$this->assertDatabaseHas('tasks', ['text' => 'second task']);
});
}
 
/** @test */
public function remove_tasks()
{
$task = factory(Task::class)->create(['user_id' => $this->user->id]);
$this->browse(function (Browser $browser) {
$browser
->loginAs($this->user)
->visit('/')
->waitForText('Tasks');
 
$browser->click("@remove-task1")
->pause(500)
->assertDontSee('test task');
});
$this->assertDatabaseMissing('tasks', $task->only(['id', 'text']));
}
 
/** @test */
public function complete_tasks()
{
$task = factory(Task::class)->create(['user_id' => $this->user->id]);
$this->browse(function (Browser $browser) use ($task) {
$browser
->loginAs($this->user)
->visit('/')
->waitForText('Tasks')
->click("@check-task{$task->first()->id}")
->waitFor('.line-through');
});
$this->assertNotEmpty($task->fresh()->is_completed);
}
}

请记住,您正在处理 javascript 元素和异步请求,因此您可以使用 waitForText('some text')waitFor('.some-selector')pause($ms = 500) 等方法来“暂停”执行,直到后端响应返回,DOM 才会由 Vue 组件更新。

选择器 @[selector-name] 引用 Vue 组件中任何 HTML 标签上的 dusk="" 属性。

现在您可以运行浏览器测试套件。

$php artisan dusk
 
PHPUnit 7.0.0 by Sebastian Bergmann and contributors.
 
... 3 / 3 (100%)
 
Time: 24.57 seconds, Memory: 16.00MB
 
OK (3 tests, 8 assertions)

Laravel dusk 的另一个很酷的功能是,您可以在出现错误时查看页面的最后状态。

Dusk 会将每个失败的屏幕截图存储在 Tests/Browser/screenshots 文件夹中。

最终结论

想一想,这些浏览器测试能覆盖多少内容

  • 测试 API 端点
  • 测试控制器
  • 测试 Javascript/Vue 组件和行为
  • 测试身份验证

那么,我建议 Laravel 开发人员如何开始测试?开始测试给定功能的整个请求/响应周期,而无需模拟。– Taylor Otwell

当然,这并不意味着您应该只使用“浏览器测试”,而是要思考并应用最适合您的策略,那种能够带来最佳效果并为您的应用程序创造最大价值的策略。

您可以应用的不同测试策略并不相互排斥,如果您认为您的项目需要浏览器测试、单元测试、功能测试等,请尽情使用,这没有硬性规定。

无论如何,如果您使用的是 Laravel,无论使用的是 Vue 还是其他任何 Javascript 框架(即使是普通的 javascript),而且您不是 Javascript 专家,但您精通 PHP,至少在我看来,这是最简单的开始方法。

要详细了解 Laravel Dusk,请阅读 此处的官方文档

Jeff photo

我是一名全栈 Web 开发人员,也是一名兼职作家。

您可以在 https://medium.com/@jeffochoa 上找到我的更多作品。

Cube

Laravel 时事通讯

加入 40k+ 其他开发人员,绝不错过任何新的提示、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发者必备的代码运行工具。使用 AI、自动完成和即时反馈在本地和生产环境中进行调试。

Tinkerwell
No Compromises logo

无妥协

来自 No Compromises 播客的两名经验丰富的开发人员 Joel 和 Aaron 现已开放接单,为您的 Laravel 项目提供服务。 ⬧ 固定价格 7500 美元/月。 ⬧ 无冗长的销售流程。 ⬧ 无合同。 ⬧ 100%退款保证。

无妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,确保您的 Web 应用程序成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,让您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

只需 2500 美元/月,即可为您的项目配备一名拥有 4-6 年经验的资深 Laravel 开发人员。获得 160 小时的专业服务和 15 天无风险试用。立即预约通话!

Bacancy
Lucky Media logo

Lucky Media

立即获得幸运 - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的强大功能带到 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 招聘网站

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动套件

SaaSykit 是一个 Laravel SaaS 启动套件,包含运行现代 SaaS 所需的所有功能。支付、精美结账、管理面板、用户仪表板、身份验证、预制组件、统计信息、博客、文档等等。

SaaSykit: Laravel SaaS 启动套件
Rector logo

Rector

您无缝升级 Laravel、降低成本和加速创新的合作伙伴,帮助成功企业取得成功。

Rector
MongoDB logo

MongoDB

通过强大的 MongoDB 和 Laravel 集成增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务性、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解灵活、现代的 MongoDB 数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的无依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的站点搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 软件包在您的 Laravel 应用程序中添加评论

阅读文章