使用 Laravel Dusk 测试 Vue 组件
发布时间 作者 Jeff
为项目添加测试总是有利于不同的方面,但选择正确的策略对于许多开发人员来说可能是一场斗争。
当您使用不同的工具或框架时,问题会加剧,虽然“尽可能多地进行测试”在理论上听起来是个好主意,但在实践中可能大相径庭。
以下来自 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) => { this.tasks.push(response.data); this.newTask = ''; }).catch((e) => console.error(e)); }, completeTask(task) { let status = ! task.is_completed; axios.put(\`/api/task/${task.id}\`, { is_completed: status }).then((response) => { task.is_completed = response.data.is_completed }).catch((e) => console.error(e)); }, checked(task) { return task.is_completed; }, removeTask(index, task) { axios.delete(\`/api/task/${task.id}\`) .then((response) => { this.tasks = [ ...this.tasks.slice(0, index), ...this.tasks.slice(index + 1) ]; }).catch((e) => 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,请阅读 此处的官方文档。