基于 TDD 模式编写 Vue 评论组件(下):Axios 请求后端接口测试


在本篇教程中,学院君将以评论创建接口为例,演示如何在 Vue 组件中为通过 Axios 调用后端接口编写测试用例。

准备工作

后端接口数据格式统一

开始之前,我们先将后端评论创建接口返回数据格式进行统一,目前表单验证失败、评论创建成功、数据库异常返回的接口数据格式都是不一样的,这会给前端接口调用和前后端接口联调造成诸多不便。

这里学院君在 app/Http/Controllers 目录下新建了一个 DataTransformer Trait 来简单处理控制器动作返回的接口数据格式:

<?php
namespace App\Http\Controllers;

trait DataTransformer
{
    public function generateResponseData($success, $message, $data = null)
    {
        return [
            'success' => $success,   // 是否成功
            'message' => $message,   // 提示消息
            'data' => $data          // 如果成功则返回成功数据,否则显示错误明细(验证表单时使用)
        ];
    }
}

如代码所示,现在将所有后端接口返回的数据统一为三个字段,分别标识是否成功、消息文案和数据明细。然后我们将 CommentControllerstore 方法改造如下:

<?php

namespace App\Http\Controllers;

use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;

class CommentController extends Controller
{
    use DataTransformer;

    ...

    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'content' => 'required'
        ]);
        if ($validator->fails()) {
            return response()->json($this->generateResponseData(false, '表单验证失败', $validator->errors()))
                ->setStatusCode(422);
        }
        $data = $validator->getData();
        try {
            $comment = new Comment($data);
            if ($comment->save()) {
                return $this->generateResponseData(true, '评论保存成功', $comment);
            }
            Log::warning('未能保存评论数据到数据库: ' . json_encode($data));
            return response()->json($this->generateResponseData(false, '评论保存失败'))
                ->setStatusCode(400);
        } catch (\Throwable $ex) {
            Log::error('保存评论数据到数据库出现异常:' . $ex->getMessage());
            return response()->json($this->generateResponseData(false, '保存数据异常'))
                ->setStatusCode(500);
        }
    }

现在创建评论接口不管是字段规则验证、数据保存成功还是数据库处理异常,都会返回统一的 JSON 格式数据,并且不同的响应对应不同的状态码,从而方便通过响应状态码快速识别响应结果:

{
    "success": boolean,
    "message": string,
    "data": object|null
}

通过 cURL 命令进行验证

为了方便在命令行测试后端接口响应,可以在 app/Http/Middleware/VerifyCsrfToken.php 中间件中取消该路由的 CSRF 防护:

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        "comments"
    ];
}

当然,如果该路由注册在 routes/api.php 中,则不需要这么做,因为所有 API 路由都没有应用这个中间件。

接下来,就可以通过 curl 命令在终端测试修改后的评论创建接口返回的数据格式了,我们先看表单验证失败的响应:

curl -i -H "Accept: application/json" -H "Content-type: application/json" -X POST  -d '{"content":""}'  http://127.0.0.1:8000/comments

-w1436

状态码依然是 422,响应实体中包含了 JSON 格式的错误信息。

再来看评论创建成功的响应信息:

curl -i -H "Accept: application/json" -H "Content-type: application/json" -X POST  -d '{"content":"测试评论"}'  http://127.0.0.1:8000/comments

-w1439

响应状态码是 200,响应实体中包含了 JSON 格式的响应数据,评论信息位于 data 属性中。

最后再来看看数据库异常情况下的响应数据(修改数据库连接信息或者在存储时将 content 属性置空即可模拟数据库处理异常):

-w1437

响应状态码是 500,响应实体中可以通过 message 字段获取错误信息。

前端测试用例编写

调整评论组件代码

对后端接口有了总体的认识之后,接下来我们在 CommentComponent 组件中调整 addNewComment 方法,将评论列表数据添加逻辑改为请求后端评论创建接口获取:

addNewComment() {
    axios.post('comments', {
        content: this.content
    }).then(resp => {
        let comment = resp.data.data;
        this.comments.push(comment);
        this.content = '';
    }).catch(error => {
        console.log(error);
    });
},

如果你在终端运行着 npm run watch-test,此时会收到报错信息:

-w825

这是因为之前是同步添加评论数据到评论列表属性的,现在改为通过 Axios 请求后端评论创建接口设置,变成了异步模式,而且这个异步模式不同于之前 Vue 组件 DOM 的异步刷新机制,是一个完全不可控的行为(比如网络异常),因此也不能简单通过 Vue.nextTick 回调进行测试。

要测试这种通过对外部接口的调用进而实现 Vue 组件本身的更新,和后端测试外部 HTTP 接口一样,需要通过拦截 Axios 请求返回伪造的响应数据来实现,只要返回的数据格式满足上面后端接口约定的 JSON 数据格式即可。

安装请求测试伪造库

JavaScript 生态中有很多类似的请求测试伪造库,比如 Axios 官方提供的针对 Axios 请求进行模拟和测试的 moxios,以及独立的第三方请求测试伪造库 sinon 等,这里我们主要测试的 Axios 请求,所以使用 moxios 即可。

使用之前,需要先通过 NPM 安装它:

npm install moxios --save-dev

编写 Axios 请求测试用例

接下来,就可以基于 Mocha + moxios 编写 Axios 请求测试用例了,先打开 setup.js,定义全局变量 axios,以便可以在所有测试用例中使用:

...
global.axios = require('axios');

然后打开评论测试文件 comment.spec.js,按照 moxios 官方示例 编写针对 comments 接口的请求测试用例:

...
import moxios from "moxios"

describe('CommentComponent.vue', () => {
    let wrapper;

    beforeEach(() => {
        moxios.install(axios)

        wrapper = mount(CommentComponent, {
            comment: {
                content: '',
                voted: false
            },
            comments: []
        })
    })

    afterEach(() => {
        moxios.uninstall(axios)
    })

    ...

    it('click submit button will render comment in comments list', function (done) {
        // Given
        expect(wrapper.find('ul.comments').isVisible()).toBe(false);
        let comment = '大家好,我是学院君。';
        wrapper.find('textarea[name=content]').element.value = comment;
        wrapper.find('textarea[name=content]').trigger('input');

        // When
        wrapper.find('button[type=submit]').trigger('submit');

        // Then
        moxios.wait(() => {
            // 拦截最近一次请求
            let request = moxios.requests.mostRecent();
            // 针对该请求返回伪造的响应数据(请求不会到达服务器直接返回)
            request.respondWith({
                status: 200,
                response: {
                    success: true,
                    message: '评论保存成功',
                    data: {
                        id: 1,
                        content: comment,
                        voted: false
                    }
                }
            }).then(() => {
                expect(wrapper.vm.comments.length).toEqual(1);
                expect(wrapper.vm.comments[0].content).toContain(comment);
                wrapper.vm.$nextTick(() => {
                    // 需要将这两个断言放到 Vue.nextTick 中执行,因为它们需要在 DOM 刷新之后才会生效
                    expect(wrapper.find('ul.comments').isVisible()).toBe(true);
                    expect(wrapper.find('ul.comments').html()).toContain(comment);
                });
                done();
            })
        });
    });
    
    ...
});

主要调整在 Then 部分,通过在 moxios.wait 回调函数中拦截最近一次请求,然后针对该请求返回伪造的响应数据,最后在 then 回调中编写断言代码即可。注意到我们在该测试用例回调中传入了一个 done,并且在断言的最后执行了 done() 方法,用于标识请求处理完成,退出 moxios.wait,这个逻辑不能省略。

另外,在整个用例的 beforeEachafterEach 方法中,分别进行了 moxios 库的安装和卸载工作,在这两个操作中必须带上 axios 实例,否则测试不会通过。

如果你运行了 npm run watch-test,此时可以看到测试通过,说明我们的 addNewComment 方法调用没有问题,接下来,可以与后端接口直接进行联调了:

-w905

当然,学院君这里只是抛转引玉,moxios 还有更多功能,比如定义桩请求和响应,并且支持正则路由匹配(这在测试更新某个项目或者获取项目详情请求时很有用),你可以查看官方示例代码进行试验,非常简单,这里就不一一演示了。

至此,我们的 Vue 组件测试驱动开发入门之旅就告一段落了,接下来,我们将正式进入单页面应用实战之旅。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 基于 TDD 模式编写 Vue 评论组件(中):父子组件之间的通信测试

>> 下一篇: 基于 Vue Router 构建单页面应用项目骨架