# 简介

最近在做一个项目的重构,技术选型为vue-cli 3.0 + typescript + vue-router + sass.因为我负责的模块比较少比较简单,所以老大让我先把负责部分的测试代码写好。至此我才第一次接触到测试代码,我们项目使用的测试工具是jest,与vue官方出的单元测试工具库vue-test-utils配合使用。第一次接触测试代码,开始的时候还是一脸懵逼,有种学习一门新语言的赶脚。经过几天的摸索之后学会了简单的编写测试代码,并对几种情况进行特殊处理。本文是一篇vue单元测试的基础入门文章,只介绍测试代码,需要了解搭建测试框架的朋友可以自行参阅vue-test-utils官方文档 (opens new window)等资料。

# 1.什么是测试代码

简言之,测试代码就是通过代码模拟一个vue组件的运行环境,使被测试的组件在这个环境下运行看是否能够得到期望的运行结果。如果运行结果与期望的结果相同,则说明该测试用例通过。下面我们来看一个最简单的实例:

  • 一个简单的vue组件的测试
// message.vue
<template>
  <div>
    <p>{{message}}</p>
  </div>
<template>
<script>
  export default{
    data(){
      return{
        message: 'a test component',
      }
    }
  }
</script>

上面这个最简单的vue组件,就是将data中的message值渲染到p标签中去。对与这样的组件,测试代码如下

import { mount } from '@vue/test-utils';
import message from './message.vue';
describe('测试message.vue组件的测试套件,可含有多个测试用例', () =>{
  it('这是测试message组件p标签能否正常渲染文字的一个测试测试用例', () => {
    const wrapper = mount(message, {}) // 使用mount可以创建一个包涵被挂载和渲染的一个实例
    expect(wrapper.find('p').text()).toBe('a test component') // expect是jest中的断言,即判断该语句前后是否相等
  })
})

上面这句expect().toBe()断言,用于判断我们用message.vue组件生成的实例中,p 标签中的文字是否等于组件 data 中的 message 的值'a test component',如果相等,则说明此断言为真。

  • 一个稍复杂组件的测试
// count.vue
<template>
  <div>
    <p>{{count}}</p>
    <button @click="add">增加</button>
  </div>
</template>
<script>
export default {
  data() {
    return{
      count: 0
    }
  },
  methods: {
    add() {
      this.count++
    }
  },
}
</script>

上面是一个带有简单交互的vue组件,count初始值为0,用户点击一次增加按钮,p标签中的值即加一。对于这样的组件我们的测试代码为

import { mount } from '@vue/test-utils';
import count from 'count.vue';
describe('count.vue组件', () => {
  it('测试count组件能否正常显示并增加', () => {
    const wrapper = mount(count, {}) // 使用 mount 创建一个vue组件实例 wrapper
    expect(wrapper.find('p').text()).toBe(0); // 判断p标签中的值是否为初始化0
    wrapper.find('button').trigger('click'); // 使用trigger('click')模拟用户的点击操作
    expect(wrapper.find('p').text()).toBe(1); // 经过模拟点击操作后,count的值应该增加成为1
  })
})

通过上面的例子我们看到,测试代码可以模拟出用户的操作,通过对操作之后的结果进行断言,能判断出该组件能否通过测试。而且一个测试用例中可以有多个断言,只有全部断言通过才说明该组件实例通过测试。只要有一个断言未通过,则说明该组件实例未通过测试。

# 2.测试带有回调的异步请求

在项目中我们很常见到vue组件内向接口请求数据的情况,那么我们如何测试这种异步请求呢。官方也给了我们实例代码:

// axios模拟
export default{
  get: () => Promise.resolve({ date: 'value' })
}
// getValue.vue
<template>
  <button @click="fetchResults" />
</template>
<script>
  import axios from 'axios'
  export default {
    data() {
      return {
        value: null
      }
    },
    methods: {
      async fetchResults() {
        const response = await axios.get('mock/service')
        this.value = response.data
      }
    }
  }
</script>
// getValue.test.js
import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios') // jest 模拟axios库

describe('getValue.vue组件', () => {
  it('点击button时,异步获取接口返回的value值', done => {
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click')
    wrapper.vm.$nextTick(() => { // 使用$nextTick 在Promise执行后再进行断言
      expect(wrapper.vm.value).toBe('value') // 对异步获取的数据进行断言,判断获取的值与期望是否都相等
      done() // 使用done()结束回调
    })
  })
})

上面的这个官方demo是通过调用组件内的接口,并对接口返回的数据进行断言。一开始我也照着官方给的代码使用,但是很快发现了一些随之出现的问题。

# 1. 调用组件本身的接口,意味着需要在组件内部传入接口请求时所需要的参数,这个数据我们可以自己传入创建的vue实例中,但是如果对于全部接口都自己传入数据会非常麻烦。而且还分成传入的参数正确或错误等不同情况。
# 2. 调用vue组件本身的http请求测试,必须得有后台接口配合,不能独立出来测试。而且测试会对后台产生真实的请求记录,因为我们开发过程中不知道会测试多少次,所以会给后台添加很多无用的请求记录。
# 3. 因为公司开发项目时,使用的是docker创建的虚拟容器作为运行环境,配置了网络地址转发,这使得我vue组件中的请求无法成功,也是这条因素使得我不能使用官方给的测试方法。

然后我就请教了我们的老大,在他的帮助下我使用另一种方法测试异步请求,做法是拦截组件中的异步请求,使用自己模拟的http请求。代码如下:

// 需要改造一下我们的 axios 请求
import axios from 'axios'
export getValue(...arg){
  return axios.get('mock/service',..arg).then(res=>{
    Promise.resolve({ date: 'value' })
  })
}
// getValue.vue
<template>
  <button @click="fetchResults" />
</template>
<script>
  import getValue from 'axios'
  export default {
    data() {
      return {
        value: null
      }
    },
    methods: {
      fetchResults() {
        getValue(...arg).then(res=>{
          this.value = res.date
        })
      }
    }
  }
</script>
// getValue.test.js
import { shallowMount } from '@vue/test-utils'
import * as svc from 'axios'
import Foo from './Foo'

describe('getValue.vue组件', () => {
  it('点击button时,异步获取接口返回的value值', done => {
    const getValue = jest.spyOn(svc, 'getValue') // 使用jest.spyOn()创建一个mock函数
    getValue.mockReturnValueOnce(Promise.resolve({data: 'value')) // 模拟我们自己mock函数的返回值
    const wrapper = shallowMount(Foo)
    wrapper.find('button').trigger('click') // 模拟用户点击事件
    wrapper.vm.$nextTick(() => { // 使用$nextTick 在Promise执行后再进行断言
      expect(getValue).toBeCalled(); // 断言是否请求了自己mock的getValue函数
      expect(wrapper.vm.value).toBe('value') // 对异步获取的数据进行断言,判断获取的值与期望是否都相等
      done() // 使用done()结束回调
    })
  })
})

jest.spyOn()方法创建一个mock函数,这个mock的函数会在组件的接口请求的时候被执行,并返回我们给mock函数添加的返回值,通过判断这个mock函数是否被执行,以及组件获取的返回值与我们给mock函数添加的返回值是否相等就可以判断组件的异步请求是否能够正确执行。通过这种方式,我们来测试异步组件。

实际上我的同事,之前也写过一篇在react项目中使用jest测试的文章,其中也介绍了使用jest.spyOn()来测试异步请求的情况。感兴趣的话可以去这里 (opens new window)结合了解一下。

# 3. 测试在有路由情况下的vue组件

vue 官方也给出了vue-test-util 配合 vue-router 使用的文档 (opens new window)。我工作中出现的一个情况是要测试在某个路由地址下,<router-view> 加载的子组件的测试。但是到目前为止本人还没有按照官方的实例跑通过测试,很尴尬,也许是我打开的姿势不对,等之后正确实现之后会把方法再补上。下面我介绍一个对这种情况测试的非官方的写法。

import Vue from 'vue'
import VueRouter from 'vue-router'
import totest from 'src/components/totest'

describe('totest.vue', () => {
  it('should totest renders stuff', (done) => {
    Vue.use(VueRouter)
    const router = new VueRouter({routes: [ // 定义路由,其中使用了被测试组件
      {path: '/totest/:id', name: 'totest', component: totest},
      {path: '/wherever', name: 'another_component', component: {render: h => '-'}},
    ]})
    const vm = new Vue({ // 自己新建一个带 router 且有 router-view 的vue实例
      el: document.createElement('div'),
      router: router,
      render: h => h('router-view')
    })
    router.push({name: 'totest', params: {id: 123}}) // 使用测试组件的路由
    Vue.nextTick(() => {
      console.log('html:', vm.$el)
      expect(vm.$el.querySelector('h2').textContent).to.equal('Fred Bloggs');
      done()
    })
  })
})

上面这个测试代码很巧妙的新建了一个使用router的vue实例,然后把被测试组件加到路由中去,当改变路由地址时,被测试组件就会被执行。此时可以对被测试组件进行断言。

# 4. 其他问题

在开发过程中还遇到了一些其他问题,如:

# 1. vue项目使用了element组件,测试代码报错,类似于 el-button 未注册
# 2. 项目使用了vue-i18n做国际化,测试代码报错,报错信息为 vm._$t is not a function...
# 3. 使用国际化vue-i18n的时候,vue组件代码里如果有 i18n.locale的话测试代码会提示无法找到locale属性

..........

上面我列出来的这三个问题,并没有给出具体的解决办法。12解决比较简单,3目前还不知道如何处理...之后请教一下我们的大佬或者自己查阅下资料,等解决之后再来更新文章。因为每个人的项目不一样,可能我遇到的问题别人并不会遇到,所以还是对着报错自己查照调试吧。这也能更加完善你自己的项目代码。本人也是小白,第一次学习写测试代码,之后有了更深入的了解之后会更新这篇文章。

# 参考文章

vue-test-utils官方 (opens new window)

一篇文章学会 Vue 项目单元测试 (opens new window)


作者简介: 宫晨光,人和未来 (opens new window)大数据前端工程师。