单元测试好处
- 提供描述组件行为的文档
- 节省手动测试的时间
- 减少研发新特性时产生的 bug
- 改进设计
- 促进重构
自动化测试使得大团队中的开发者可以维护复杂的基础代码。
TDD & BDD
TDD(Test Driven Development)测试驱动开发
TDD 的思想是根据需求先写测试用例,依照测试用例再去写功能代码。当增加或者修改某一项需求的时候,需要先修改测试用例,再依照测试用例去修改代码逻辑。
基本步骤:
- 编写测试用例
- 运行测试,测试用例无法通过测试
- 编写代码,使测试用例通过测试
- 优化代码,完成开发
- 重复以上步骤
BDD(Behavior Driven Development)行为驱动开发
与 TDD 相反,BDD 是根据需求先进行开发,等到该功能开发完毕后,再开始编写测试代码进行测试。
基本步骤:
- 编写代码
- 编写测试用例,测试无法通过
- 编写代码,使测试用例通过
- 优化代码,完成开发
- 重复以上步骤
目前我们采用的是BDD模式。
Jest配置
如果是使用vue-cli构建的项目,在初始化时会询问是否使用单元测试,只需按步骤选择jest即可,会自动安装Vue Test Utils,它是 Vue.js 官方的单元测试实用工具库,为 jest
和 Vue
提供了一个桥梁,暴露出一些接口,让我们更加方便的通过 Jest
为 Vue
应用编写单元测试。
在生成的项目根目录下,会有一个 jest.config.json
文件,cli
自动生成所用的预设是@vue/cli-plugin-unit-jest
。我们可以在这里对 jest
进行个性化的配置。以下是一个配置文档的例子。配置文档具体参数说明
module.exports = {
// 告诉jest需要解析的文件
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
// 告诉jest去哪里找模块资源,同webpack中的modules
moduleDirectories: [
'src',
'node_modules'
],
// 告诉jest针对不同类型的文件如何转义
transform: {
'^.+\\.(vue)$': '<rootDir>/node_modules/vue-jest',
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest',
'^.+\\.ts?$': 'ts-jest'
},
// 告诉jest在编辑的过程中可以忽略哪些文件,默认为node_modules下的所有文件
transformIgnorePatterns: [
'<rootDir>/node_modules/'
+ '(?!(vue-awesome|veui|resize-detector|froala-editor|echarts|html2canvas|jspdf))'
],
// 别名,同webpack中的alias
moduleNameMapper: {
'^src(.*)$': '<rootDir>/src/$1',
'^@/(.*)$': '<rootDir>/src/$1',
'^block(.*)$': '<rootDir>/src/components/block/$1',
'^toolkit(.*)$': '<rootDir>/src/components/toolkit/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
// 告诉jest去哪里找我们编写的测试文件
testMatch: [
// '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
'**/tests/unit/**/Test.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
// 在执行测试用例之前需要先执行的文件
setupFiles: ['jest-canvas-mock']
};
其中需要注意的一点是,因为项目中用到了 veui,node_modules 中引用的是源码,未经过 babel 转义。因此需要在 transformIgnorePatterns 中告诉jest,需要对其进行编译。其他引入的第三方库同理。
Vue Test Utils
在编写测试用例之前,我们先来简单了解一下 Vue Test Utils
的 几个 API
以及 Wrapper
。
mount: 创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
shallowMount: 与mount作用相同,但是不渲染子组件
render: 将一个对象渲染成为一个字符串并返回一个 cheerio 包裹器
createLocalVue: 返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类
wrapper: 一个 wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。
wrapper.vm: 可以访问一个实例所有的方法和属性
wrapper.setData() : 同Vue.set()
wrapper.trigger(): 异步触发事件
wrapper.find(): 返回DOM节点或者Vue组件
具体的 API 和 Warpper方法和实例 可以参考这里。
Demo
// test.spec.js
import { shallowMount } from '@vue/test-utils'
import Test from '@/components/Test'
const factory = (values = {}) => {
return shallowMount(Test, {
data () {
return {
...values
}
}
})
}
describe('Foo', () => {
it('renders a welcome message', () => {
const wrapper = factory()
expect(wrapper.find('.message').text()).toEqual("Welcome to the Vue.js cookbook")
})
it('renders an error when username is less than 7 characters', () => {
const wrapper = factory({ username: '' })
expect(wrapper.find('.error').exists()).toBeTruthy()
})
it('renders an error when username is whitespace', () => {
const wrapper = factory({ username: ' '.repeat(7) })
expect(wrapper.find('.error').exists()).toBeTruthy()
})
it('does not render an error when username is 7 characters or more', () => {
const wrapper = factory({ username: 'Lachlan' })
expect(wrapper.find('.error').exists()).toBeFalsy()
})
it('snapshot test', () => {
const wrapper = factory()
expect(wrapper.html()).toMatchSnapshot()
})
})
生成测试覆盖率报告
可以通过配置 jest.config.json
增加来生成测试覆盖率报告,生成报告会降低单测的速度,配置中默认是关闭的,需要手动开启。
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
// 开启测试报告
collectCoverage: true,
// 统计哪里的文件
collectCoverageFrom: ["**/src/components/**", "!**/node_modules/**"]
}
生成的报告在根目录的 coverage
文件夹下,可以通过 package.json
配置命令行打开测试报告或者在控制台查看。
"scripts": {
"coverage": "open ./coverage/lcov-report/index.html"
},

%Stmts(statement coverage): 语句覆盖率,是否每个语句都执行了?
%Branch(branch coverage): 分支覆盖率,是否每个if代码块都执行了?
%Funcs(branch coverage): 函数覆盖率,是否每个函数都调用了?
%Lines(line coverage): 行覆盖率,是否每一行都执行了?