[ Laravel从入门到精通 ] 测试系列 —— 通过测试驱动开发构建待办任务项目(二):前端功能和浏览器...

栏目: 编程语言 · PHP · 发布时间: 6年前

内容简介:在上篇教程中,学院君已经完成了待办任务项目后端 API 接口的编写和功能测试,现在,我们开始编写 Vue 组件来实现前端的交互界面。首先在在这个 Vue 组件中,我们会通过父组件传入的

在上篇教程中,学院君已经完成了待办任务项目后端 API 接口的编写和功能测试,现在,我们开始编写 Vue 组件来实现前端的交互界面。

编写前端 Vue 组件

首先在 resources/js/components 目录新增一个 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>

<script>
    export default {
        props: ['initialTasks'],
        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));
            }
        }
    }
</script>

在这个 Vue 组件中,我们会通过父组件传入的 initialTasks 属性来完成待办任务列表的渲染,然后我们还可以在组件中通过 Axios 库与后端 API 接口交互实现新增任务,移除任务,以及将任务标记为已完成。

接下来,我们需要将这个 Vue 组件注册到全局 Vue 实例,这个工作在 resources/js/app.js 中完成:

...

Vue.component('tasks-component', require('./components/TasksComponent.vue').default);

...

编写前端视图模板

将 CSS 框架切换为 Tailwind CSS

到这里还没有结束,我们还要将上述 Vue 组件嵌入到视图模板中才能在前端显示出来。为此,我们还要编写相应的前端视图文件和布局文件,Laravel 默认的 CSS 框架是 Bootstrap,这里学院君想换个口味,使用 Tailwind CSS 来替代框架预设的 Bootstrap 样式( Tailwind CSS 对应的中文文档 在这里 ),这可以通过一个 Laravel 扩展包来快速切换,我们通过 Composer 来安装这个扩展包:

composer require laravel-frontend-presets/tailwindcss

然后运行如下 Artisan 命令执行切换:

php artisan preset tailwindcss

该命令会将 package.json 中 Bootstrap 相关扩展包替换成 Tailwind 的,并且删除 resources/sass 目录,将 Tailwind 资源文件发布到 resources/cssresources/js 目录,更新 resources/views/welcome.blade.php 视图文件和 webpack.mix.js 文件。

如果你需要更新框架自带的用户认证相关视图脚手架代码,还可以运行如下命令进行切换,建议执行这个命令,因为它会替我们生成后面要用到的认证路由、控制器和视图相关文件(运行这个命令就不必运行上一个 preset 命令了):

php artisan preset tailwindcss-auth

至此,从 Bootstrap 框架切换到 Tailwind CSS 框架的工作就完成了。

编写视图模板文件

我们将任务列表 Vue 组件的渲染放到 resources/views/home.blade.php 视图文件中,修改该视图模板代码如下:

@extends('layouts.app')

@section('content')
    <div class="container px-4 sm:px-0 mx-auto py-8">
        <tasks-component :initial-tasks="{{ $tasks }}"></tasks-component>
    </div>
@endsection

该视图继承自 layouts.app 布局,我们在里面嵌入了前面注册的 tasks-component 组件,并且通过 initial-tasks 属性将控制器传过来的任务列表传入 Vue 组件(就是前面提到的 initialTasks 属性),由于我们把前端逻辑都封装到 Vue 组件中了,所以这个视图模板非常简洁。

编译前端资源

到这里,前端视图和 Vue 组件都编写好了,接下来我们需要编译前端资源,以便让前端视图可以正常渲染和使用,首先需要运行如下命令安装 package.json 中定义的前端资源依赖:

npm install

在编译前端资源前,需要对前端编译编排文件 webpack.mix.js 稍作修改:

const mix = require('laravel-mix');

/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

require('laravel-mix-purgecss')

mix.js('resources/js/app.js', 'public/js')
    .postCss('resources/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 install postcss-import postcss-cssnext

做好了以上准备工作,接下来就可以运行如下命令来编译前端资源了:

npm run dev

编写后端代码

为了让前端视图可以正常渲染,页面交互功能可以正常使用,我们最后还要对后端代码做一些调整。

HomeController

我们希望用户登录之后才能访问待办任务列表,用户登录之后默认跳转的路由是 /home ,该路由对应的控制器方法是 HomeController@index (在 routes/web.php 中可以看到),所以我们在 app/Http/Controllers/HomeController.php 中编写相应的业务逻辑代码如下:

public function index()
{
    $tasks = auth()->user()->tasks->all();
    return view('home', ['tasks' => json_encode($tasks)]);
}

我们将认证用户名下关联的任务列表作为参数传递给 home 视图,为了让这段代码生效,还要在 User 模型类中新增一个 tasks 关联方法:

public function tasks()
{
    return $this->hasMany(Task::class);
}

单页面应用的认证实现

到目前为止,我们编写的这个待办任务项目算得上是个前后端分离的单页面应用,因为任务的增、删、改都是通过前端组件调用后端 API 接口异步实现的,后端 API 接口需要基于 API 进行认证,而我们之前介绍 API 认证时正好介绍过这种场景的认证实现: 用户认证与授权系列 —— 通过 Passport 实现 API 请求认证:单页面应用篇 ,这里我们同样借鉴这个思路来实现基于 Session 的登录认证与基于 Passport 实现的 API 认证的一体化。

首先打开 config/auth.php ,将 guards 配置项中的 api.driver 配置值修改为 passport

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
        'hash' => false,
    ],
],

然后在 app/Http/Kernel.php 中,添加 \Laravel\Passport\Http\Middleware\CreateFreshApiToken 中间件到 web 中间件组:

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class
    ],

    ...

通过这个中间件可以实现用户通过表单登录后将访问令牌保存到 Cookie 中,以便在 API 认证时使用,这样就完成用户 Session 认证和 API 认证的一体化了。

至此,待办任务项目前后端的功能代码都已经编写好了,下面我们可以基于 Laravel Dusk 编写浏览器测试用例了。

基于 Dusk 实现浏览器测试

初始化 Dusk

使用 Dusk 之前,先通过 Composer 安装 Dusk 扩展包:

composer require --dev laravel/dusk

然后运行如下 Artisan 命令初始化 Dusk(在 tests 命令下创建 Browser 子目录及相关示例文件):

php artisan dusk:install

编写浏览器测试用例

通过如下命令创建一个新的浏览器测试用例:

php artisan dusk:make TasksTest

该命令会在 tests/Browser 目录下创建 TasksTest.php 文件,编写该测试用例文件代码如下:

<?php

namespace Tests\Browser;

use App\Task;
use App\User;
use Tests\DuskTestCase;
use Laravel\Dusk\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class TasksTest extends DuskTestCase
{
    use DatabaseMigrations;

    protected $user;

    /**
     * 通过模型工厂初始化测试用户
     */
    protected function setUp(): void
    {
        parent::setUp();
        $this->user = factory(User::class)->create();
    }

    /**
     * 测试创建任务
     * @throws \Throwable
     */
    public function testCreateTask()
    {
        $this->browse(function (Browser $browser) {
            // 以认证用户身份测试访问待办任务首页
            $browser->loginAs($this->user)
                ->visit('/')
                ->assertSee('Tasks');

            /**
             * 测试新增一个待办任务:
             * 输入「First Task」-> 点击提交「Add」-> 提交成功后断言列表里出现刚刚新增的任务
             */
            $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']);
        });
    }

    /**
     * 测试移除任务
     * @throws \Throwable
     */
    public function testRemoveTask()
    {
        // 使用模型工厂创建一个待测试任务「Test Task」
        $task = factory(Task::class)->create([
            'text' => 'Test Task',
            'user_id' => $this->user->id
        ]);

        $this->browse(function (Browser $browser) {
            // 以认证用户身份访问首页
            $browser
                ->loginAs($this->user)
                ->visit('/')
                ->waitForText('Tasks');

            // 点击移除任务按钮,0.5秒后断言任务是否已删除(对应任务不存在)
            $browser->click("@remove-task1")
                ->pause(500)
                ->assertDontSee('Test Task');
        });

        // 断言数据库不包含对应任务确认后端删除成功
        $this->assertDatabaseMissing('tasks', $task->only(['id', 'text']));
    }

    /**
     * 测试完成任务(修改)
     * @throws \Throwable
     */
    public function testCompleteTask()
    {
        // 还是使用模型工厂创建一个测试任务
        $task = factory(Task::class)->create(['user_id' => $this->user->id]);

        $this->browse(function (Browser $browser) use ($task) {
            // 以认证用户身份访问首页并勾选任务已完成,
            // 如果 `line-through` 选择器出现则说明操作成功
            $browser
                ->loginAs($this->user)
                ->visit('/')
                ->waitForText('Tasks')
                ->click("@check-task{$task->first()->id}")
                ->waitFor('.line-through');
        });

        // 断言数据库已完成任务不为空来确认后端数据库记录已更新
        $this->assertNotEmpty($task->fresh()->is_completed);
    }
}

在该浏览器测试用例中,我们仍然使用了 DatabaseMigrations Trait 在测试用例运行前后重构和回滚所有数据库变更,以免产生脏数据,然后我们使用 setUp 方法在测试用例运行之前通过模型工厂创建一个初始测试用户,接下来编写了三个具体的测试用例,分别用于测试任务的创建、移除和修改,在这些测试用例中我们通过 $browser 实例模拟浏览器页面的访问、登录、表单输入、按钮点击等操作,从而完成相应的后端 API 调用,并且根据按钮、元素点击后页面的变化来断言相应的操作结果是否符合预期(更多断言方法与元素交互细节可以参考Dusk 文档),最后,还通过对数据库记录进行断言来确认前端操作是否生效(数据库断言及测试的更多细节请参考数据库测试文档)。

注:由于后端任务的创建、删除和修改 API 接口都需要认证后才能访问,所以我们通过浏览器实例的 loginAs 方法模拟用户 Web 登录,同时由于在 web 中间件组中应用了 CreateFreshApiToken 中间件,用户登录后将访问令牌保存到 Cookie 中,这样下次用户访问需要认证的 API 接口时就可以直接通过这个令牌判断用户已经登录了,从而实现了两种渠道认证的无缝对接。

运行浏览器测试用例

至此,浏览器测试用例编写完成,并且覆盖了所有 Vue 组件中涉及到的与后端 API 交互的方法,下面运行这个浏览器测试用例(运行之前先删除系统自带的 tests/Browser/ExampleTest.php 用例文件,因为我们已经调整过首页逻辑,所以该测试用例会运行失败),绿色代表测试通过:

[ Laravel从入门到精通 ] 测试系列 —— 通过测试驱动开发构建待办任务项目(二):前端功能和浏览器...

这样一来,说明我们编写的前端视图和 Vue 组件功能无碍,可以进行后续其他功能的迭代了。

项目整体体验

前面所有功能的编写和测试都是通过代码完成的,到目前为止,我们还不知道项目的页面是什么样子,既然前面的测试表明项目的各项功能已经通过验收,下面不妨来看下庐山真面目。

由于我们在测试用例中都使用了 DatabaseMigrations Trait,测试用例运行完成后,数据库的所有更改都回滚了,所以在体验之前,需要运行 php artisan migrate 创建所有数据表。

然后通过 http://todoapp.test 访问应用首页,经过 Tailwind CSS 渲染的首页长这样:

[ Laravel从入门到精通 ] 测试系列 —— 通过测试驱动开发构建待办任务项目(二):前端功能和浏览器...

要访问待办任务页面,需要用户先登录,为此,我们来注册个新用户:

[ Laravel从入门到精通 ] 测试系列 —— 通过测试驱动开发构建待办任务项目(二):前端功能和浏览器...

注册成功后,页面跳转到 /home 路由,此时待办任务列表为空:

[ Laravel从入门到精通 ] 测试系列 —— 通过测试驱动开发构建待办任务项目(二):前端功能和浏览器...

由于我们的前端功能和后端功能都已经通过测试验收,所以大胆的对任务进行增删改查好了:

[ Laravel从入门到精通 ] 测试系列 —— 通过测试驱动开发构建待办任务项目(二):前端功能和浏览器...

到这里,我们的测试驱动开发项目就告一段落,但是这里不是终点,后续介绍广播、缓存、队列、事件时还会基于此项目进行迭代。下一篇,我们将探索如何对 Laravel 项目进行持续集成。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web 2.0 Architectures

Web 2.0 Architectures

Duane Nickull、Dion Hinchcliffe、James Governor / O'Reilly / 2009 / USD 34.99

The "Web 2.0" phenomena has become more pervasive than ever before. It is impacting the very fabric of our society and presents opportunities for those with knowledge. The individuals who understand t......一起来看看 《Web 2.0 Architectures》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具