Vue 的单元测试探索(一)

最近在进行业务项目的重构,使用到 Vue 和单元测试相关知识,这里想把单元测试在 Vue 业务组件中的实践经验分享给大家,希望能够对大家有所帮助。本篇文章着重讲的是测试思路和 case 写法,要求读者有 Vue 项目实践经验,对前端测试框架 Karma、断言库、代码覆盖率等知识有一定的了解。

环境搭建

使用 vue-cli 可以快速生成一个 Vue 项目,其中包含了 Webpack 和 Karma 以及覆盖率统计的配置,我们不需要做过多的工作,就可以直接开始编写业务代码和单元测试。在这里就不多做介绍了,详情请参考链接 vuejs/vue-cli

测试关注点

我们今天主要讲的是 Vue 业务组件的单元测试,因此我们的侧重点主要包括模板渲染、方法执行(其中包括 watch,computed,filter, 生命周期函数等)两个方面。

编写单元测试

一、准备工作

业务组件和 UI 组件的不同之处在于,业务组件通常有更多的外部模块依赖和业务逻辑处理。比如依赖外部组件库或者插件、需要和后端 api 进行交互、依赖 router 路由中的数据,依靠store 来管理组件间的共享数据等等。因此在开始实例化组件之前,我们需要对该组件构造函数的外部依赖进行 mock 和注入 ,才能保证在实例化组件的过程中不会因为缺少外部依赖而执行异常。

组件库 mock

比如我们项目中使用到了组件库 element-ui,在组件的 template 中用到了相关的组件。我们在编写测试文件的时候,不仅要导入要测试的组件对象和 Vue,还需要引入 element-ui 组件库,才能保证组件在 compile 的时候不会报错。写法如下:

import Vue from 'vue';
import ElementUI from 'element-ui';

Vue.use(ElementUI);
api mock

举个栗子组件 A:

export default{
  data() {
    return {
      testData: '',
    };
  },
  methods: {
    async getData() {
      const id = this.$route.params.skuId; // route中获取数据
      const { data } = await AccountService.getData(id);
      const testData = data.data;
    },
  },
  computed: { // store数据管理
    ...mapState({
      storeData: state => state.order.storeData,
    }),
  },
};

我们可以看到组件中 AccountService 负责管理 api 接口调用,如果我们要测试组件中 getData 这个方法,首先就要对方法中依赖的 AccountService.getData 函数 进行mock。只有我们掌握了 api 的返回结果,才能进行后续的断言。这里我们用到的 mock 工具是 inject-loader, 能够在 Webpack 打包的时候对 AccountService 文件中内容进行 mock 。用法如下:
const inject = require('!!vue?inject!A.vue'); // 行内写法
const ComponentAwithMock = inject({
  'src/service/accountService': {  // mock整个accountService模块
    getData(reslove) {
      const data = {
        code: 200,
        msg: 'success',
        data: 'test',
      };
      reslove({ data });
// 因为项目中用到了vue-resource,真实的API返回结果会多一层包装,所以mock的时候数据包了一层
    },
  },
});
  

值得一提的是 inject-loader 可以对组件内部的任何模块依赖进行内容替换,不仅是 api 模块还可以是子 component 。

依赖注入

我们看到上一个例子组件A中用到了 store 和 router 中的数据,我们需要把这两个依赖注入到组件中。需要提一下的是如果 store 中的 action 涉及到和后端的 api 交互,举个栗子:

actions: {
  async [type.REFRESH_STORE_STATUS](context, status = 0){
    const { data } = await orderService.getData(status);
    context.commit(type.REFRESH_DATA, data.data || {});
  },
},

这时需要我们对 store 进行 mock,写法如下:

const store = new Vuex.Store({
  state: {},
  actions: {
    async [type.REFRESH_STORE_STATUS](context) {
      const data = { data: 'test' }; // 模拟数据
      context.commit(type.REFRESH_DATA, data);
    },
  },
});

然后我们就可以将 store 对象,注入到构造函数中:

const Component = Vue.extend({ ...ComponentAwithMock, store, router });

至此我们得到了 mock 过的组件构造函数 Component。

二、编写 case

初始化 vm 实例

 const vm = new Component().$mount(); // 生成实例
 const vm = new Component({ propsData: { title: '测试标题' }).$mount();
// 组件的渲染输出由它的 props 决定时,需要传入propsData

断言-模板渲染

模板的渲染一般是根据 vm.$data 来的,所以可以通过赋予 vm.$data 不同的值来断言不同的渲染结果。举个栗子:

it('should render correct dom', (done) => {
  const vm = new Component(); // 生成实例
  vm.testData = 'bye'; // 模拟不同的数据
  vm.$nextTick(() => {
    expect(vm.$el.textContent).toBe('bye!'); // 断言渲染结果
    done();
  })
})

需要注意的是 dom 的更新是异步的,所以模板断言要写在 $nextTick 里面。

断言-方法

vm 中的方法一般是 vm.$data 数据操作函数或数据处理函数等,方法主要包括 watch, computed, filter,methods, 生命周期函数等。


vm.$data 数据操作函数

changeData(data) {
  this.data = data;  // 纯操作data数据的函数
}, 

我们可以直接通过 vm 实例方法调用,然后断言 vm.$data 。还有一种是从 api 中获取数据,然后赋值给 vm.$data 。因为我们前面对 api 返回的数据进行了 mock,所以能够对 vm.$data 进行断言;

it('should exportExcel be right', () => {
  const vm = new Component().$mount();
  vm.changeData('hello world');
  expect(vm.data).to.equal('hello world');
});
数据处理函数

这类函数是指对数据进行计算、处理然后输出,比较典型的是组件中的 filter 函数,computed函数等。测试方法也比较简单,通过直接方法调用,断言返回结果。在此就不一一举栗子了。

当然有的函数功能比较复杂,可能同时包含了 vm.$data 数据操作、数据处理和其他的函数调用等等,具体的测试方法要根据情况灵活调整。所以我们在写代码时对函数拆分到最小颗粒对测试方面也有很大的意义。


结束语

由于文章篇幅和本人能力有限,有写的不对的地方,欢迎各位看官批评指正,求轻喷。感兴趣的同学可以参考下面的资源链接进行深入研究学习。

参考资源

单元测试 — Vue.js
vuejs-templates/webpack
Testing with Mocks
slideshare.net/coulix/v
编辑于 2017-05-11

文章被以下专栏收录

    只看代码的话,上 https://github.com/ElemeFe 。这一群人,关心的不是「如何写前端」而是「如何很好地运行一个 ( web ) APP」;这一群人,会在监控屏上加上弹幕,会让实习生自主招聘,会设计、编写、监控整个 APP 的生命周期;这一群人,玩的时候... 更卖力,就像从来没来过那般卖力,卖力地热爱生活。所以这些创作大多基于 ❤️