如何对vue.js单文件(.vue)进行单元测试?

最近在用karma对vue.js进行测试,但是每个组件都是.vue文件。看了下官网给的测试例子,主要也是对组件里的方法进行测试。但涉及到组件传参,异步…
关注者
343
被浏览
60,213
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏
我向来是不惮以最坏的恶意,来推测我的代码的。—— 鲁迅

为什么要单元测试

其实对我个人而言,单元测试最大的好处就是不用在修改代码之后提心吊胆的等着浏览器刷新。

  • 修改一小部分代码,就可以模拟上下文,调试一个片段
  • 运行速度也比编译整个项目得多。
  • 如果一次没改好,也不用重复操作

总的来说,我是把测试用例当作调试用的。一边写代码,一边写测试用例。

如果你的项目已经完成了,要补测试用例的话,感觉不太合算

单元测试和E2E测试

前端的测试主要有两种,单元测试和E2E测试,这里再明确一下。如果你想了解E2E测试,那么你可以关掉这个tab了。

单元测试:按空间切割,对每个组件进行测试

比如,我要测试日期输入框,那么我编写的测试用例应该包括以下部分:

  • 默认日期是否为当天
  • 当用户选择日期范围,data是否会做相应改变
  • ...

E2E测试:按时间切割,对每个流程进行测试

比如,我要测试搜索功能,那么我编写的测试用例应该模拟以下步骤:

  • 打开主页
  • 点击菜单跳转到详情页
  • 输入搜索条件
  • 点击搜索
  • 查看搜索结果是否与预期一致
  • ...

开发环境

前端的测试框架有很多,我想大部分developer的项目都应该是这样生成的吧。

vue init webpack test

如果不是,就烦劳各位看官自己配置以下环境了。主要测试库包括:

  • Karma:将项目运行在各种浏览器
  • Mocha:定义测试模块
  • Chai:断言库

你的package.json应当包含这样的script:

"unit": "jest --config test/unit/jest.conf.js --coverage",

你的test文件夹下应当包含三个配置文件,如果不太熟悉的话可以像我这样配置。

Karma的配置文件,默认即可:

// karma.conf.js
var webpackConfig = require('../../build/webpack.test.conf')

module.exports = function (config) {
  config.set({
    // 测试器环境
    browsers: ['PhantomJS'],
    // 测试使用的框架
    frameworks: ['mocha', 'sinon-chai'],
    // 结果存在哪里
    reporters: ['spec', 'coverage'],
    // 测试入口
    files: ['./index.js'],
    // 对指定文件进行预处理
    preprocessors: {
      './index.js': ['webpack', 'sourcemap']
    },
    // webpack打包规则
    webpack: webpackConfig,
    // 使用了什么中间件
    webpackMiddleware: {
      stats: 'errors-only'
    },
    // 覆盖率配置
    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })
}

引入规则,遇到编译错误首先检查这个文件:

import Vue from 'vue'
Vue.config.productionTip = false

/* eslint-disable no-extend-native */
Function.prototype.bind = require('function-bind')

const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// 这里要把样式剔除,也可以在webpack.test.conf.js里添加resolve
const srcContext = require.context('../../src', true, /^.\/(style$)/)
srcContext.keys().forEach(srcContext)

Eslint配置,默认即可:

{
  "env": {
    "mocha": true
  },
  "globals": {
    "expect": true,
    "sinon": true
  }
}

好了,接下来我们就可以在specs文件夹中编写自己的测试用例了。

编写测试用例

静态组件

我们先从最简单的组件开始,这种组件只能展示数据,只有在生命周期中数据会变化。

这个组件长这样:

<template>
  <span>{{ msg }}</span>
</template>
<script>
  export default {
    data () {
      return {
        msg: 'hello world'
      }
    },
    mounted () {
      this.msg = 'bye bye'
    }
  }
</script>

我们可以写出这样的测试用例:

/* eslint-disable */
// 测试用例可以绕过eslint检验,不然要烦死你咯

import Vue from 'vue'
import MyComponent from '@components/MyComponent'

describe('MyComponent', () => {
  // 检查mounted
  it('has a mounted hook', () => {
    expect(typeof MyComponent.mounted).to.eql('function')
  })

  // 组件实例
  const Constructor = Vue.extend(MyComponent);

  // 检查msg
  it('msg should change to bye bye', () => {
    expect(myComponent.msg).to.eql('bye bye')
  })

  // 挂载组件
  const myComponent = new Constructor().$mount();

  // 检查msg
  it('msg should in page', () => {
    expect(myComponent.$el.textContent).to.contain('bye bye')
  })
})

小技巧

这里有很多同学可能不了解vue实例有哪些属性,这里教给大家一个(调试)小技巧。我们可以使用vue-tools。

首先打开vue-tools,选中一个元素。

然后在控制台查看$vm,是不是很惊喜?可以对$vm的属性直接更改哦,会显示在页面上。

小鲜肉可以在开始的时候测试用例和浏览器并用,平缓的度过过度期。

组件有props

如果组件需要传入props怎么办呢,我们只要在构造函数中传入就可以了。

这个组件长这样:

<template>
  <span>{{ propMsg }}</span>
</template>
<script>
  export default {
    props: ['propMsg']
  }
</script>

我们可以写出这样的测试用例:

/* eslint-disable */
import Vue from 'vue'
import MyComponent from '@components/MyComponent'

describe('MyComponent', () => {
  // 组件实例
  const Constructor = Vue.extend(MyComponent);

  // 挂载组件
  const myComponent = new Constructor({ propsData: {
    propMsg: 'hello world'
  }}).$mount();

  // 检查msg
  it('msg should in page', () => {
    expect(myComponent.$el.textContent).to.contain('hello world')
  })
})

组件有异步操作

掌握了上面的内容,静态组件应该不成问题了。可是我们的业务逻辑中,很少出现data完全不变的组件。下面说明一下当data变化的时候,我们应该注意些什么。

这个组件长这样:

<template>
  <span>{{ msg }}</span>
</template>
<script>
  export default {
    data () {
      return {
        msg: 'hello world'
      }
    }
  }
</script>

我们可以写出这样的测试用例:

/* eslint-disable */
import Vue from 'vue'
import MyComponent from '@components/MyComponent'

describe('MyComponent', () => {
  // 组件实例
  const Constructor = Vue.extend(MyComponent);

  // 挂载组件
  const myComponent = new Constructor().$mount();

  // 检查msg
  it('msg should in page', () => {
    myComponent.msg = 'bye bye'
    // 需要等待更新
    Vue.nextTick(() => {
      expect(myComponent.$el.textContent).to.contain('bye bye')
    })
  })
})

组件有用户交互

当然,业务逻辑中也少不了用户的点击。这里我是用了Element-UI,因此需要引入进来。

有用户点击

这个组件长这样:

<template>
  <div>
    <span>{{ msg }}</span>
    <el-button type="primary" @click="handleChange">Change</el-button>
  </div>
</template>
<script>
  export default {
    data () {
      return {
        msg: 'hello world'
      }
    },
    methods: {
      handleChange () {
        this.msg = 'bye bye'
      }
    }
  }
</script>

我们可以写出这样的测试用例:

/* eslint-disable */
import Vue from 'vue'
import MyComponent from '@components/MyComponent'

// 这里需要引入Element-UI
import ElementUI from 'element-ui';
Vue.use(ElementUI);

describe('MyComponent', () => {
  // 组件实例
  const Constructor = Vue.extend(MyComponent);

  // 挂载组件
  const myComponent = new Constructor().$mount();

  // 检查msg
  it('msg should in page', () => {
    const button = myComponent.$el.querySelector('button')
    const clickEvent = new window.Event('click')
    button.dispatchEvent(clickEvent)
    // 需要手动监听更新
    myComponent._watcher.run()

    expect(myComponent.$el.textContent).to.contain('bye bye')
  })
})

有用户输入

我们不用真的去获取输入框,然后修改输入框的值。只要结合上面两个例子,修改model并提交就可以啦。

这个组件长这样:

<template>
  <div>
    <span>{{ msg }}</span>
    <el-input v-model="input"></el-input>
    <el-button type="primary" @click="handleChange">Change</el-button>
  </div>
</template>
<script>
  export default {
    data () {
      return {
        msg: '',
        input: 'hello world'
      }
    },
    methods: {
      handleChange () {
        this.msg = this.input
      }
    }
  }
</script>

我们可以写出这样的测试用例:

/* eslint-disable */
import Vue from 'vue'
import MyComponent from '@components/MyComponent'

import ElementUI from 'element-ui';
Vue.use(ElementUI);

describe('MyComponent', () => {
  // 组件实例
  const Constructor = Vue.extend(MyComponent);

  // 挂载组件
  const myComponent = new Constructor().$mount();

  // 检查msg
  it('msg should in page', () => {
    myComponent.input = 'test input'
    const button = myComponent.$el.querySelector('button')
    const clickEvent = new window.Event('click')
    button.dispatchEvent(clickEvent)
    // 需要手动监听更新
    myComponent._watcher.run()

    expect(myComponent.$el.textContent).to.contain('test input')
  })
})

拦截其他依赖

除了上述情况,有时候我们还会遇到需要拦截mock,拦截vuex的情况。这时候就需要引入一个inject-loader了。

这个组件长这样:

<template>
  <div class="msg">{{ msg }}</div>
</template>

<script>
// 这里需要被拦截
import getData from 'src/lib/apis/getData'

export default {
  data () {
    return {
      msg: getData.getData()
    }
  }
}
</script>

我们可以写出这样的测试用例:

/* eslint-disable */
import Vue from 'vue'
const getData = () => {
  return 'hello world'
}
// 这里使用loader之后没办法使用@components等resolve配置
const MyModuleInjector = require('!!vue-loader?inject!./MyComponent.vue')
const MyComponent = MyModuleInjector({
  'src/lib/apis/getData': { getData },
})

import ElementUI from 'element-ui';
Vue.use(ElementUI);

describe('MyComponent', () => {
  // 组件实例
  const Constructor = Vue.extend(MyComponent);
  const myComponent = new Constructor().$mount();
  // 检查msg
  it('msg should in page', () => {
    expect(myComponent.$el.textContent).to.contain('hello world')
  })
})

结束

这样就基本覆盖到了所有需求,如果有时间可以自己封装一些store、router或者mock。会让我们的test更舒服。

以上。