https://vue-test-utils.vuejs.org/zh/
TDD (Test Driven Development 测试驱动开发)
TDD开发流程
- 编写测试用例。
- 运行测试,测试用例无法通过。
- 编写代码,使测试用例通过测试。
- 优化代码,完成开发。
- 重复上述步骤
TDD的优势
- 长期减少回归bug.
- 代码质量更好(组织,可维护性)。
- 测试覆盖率高。
- 错误测试代码不容易出现。
在vue中使用jest进行测试
通过vue-cli脚手架生成项目
jest.config.js
module.exports = {
// 依次找 js、jsx、json、vue 后缀的文件
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
// 使用 vue-jest 帮助测试 .vue 文件
// 遇到 css 等转为字符串 不作测试
// 遇到 js jsx 等转成 es5
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
// 哪些文件下的内容不需要被转换
transformIgnorePatterns: [
'/node_modules/'
],
// 模块的映射 @ 开头到根目录下寻找
moduleNameMapper: {
'^@/(.*)$': '/src/$1'
},
// snapshot 怎么去存储
snapshotSerializers: [
'jest-serializer-vue'
],
// npm run test:unit 时到哪些目录下去找 测试 文件
testMatch: [
'**/__tests__/**/*.(js|jsx|ts|tsx)'
],
// 模拟的浏览器的地址是什么
testURL: 'http://localhost/',
// 两个帮助使用 jest 的插件 过滤 测试名/文件名 来过滤测试用例
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname'
]
}
HelloWorld.test.js
import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue",() => {
it("测试helloworld组件", () => {
const msg = "你好世界";
const wrapper = shallowMount(HelloWorld,{
propsData:msg
});
expect(wrapper.text()).toMatch(msg);
})
})
shallowMount
是vue test utils
中的一个方法为浅渲染,表示只渲染当前组件不关注子组件中的内容,一般用于单元测试,性能较高。与之对应的是mount
,它会将当前组件以及所有子组件都渲染,适合集成测试,性能稍差。
由上代码可知shallowMount()
方法第一个参数接收需要测试的组件,第二个参数是一个对象,它的propsData
属性就是该组件的props
,使用一个wrapper
接收shallowMount()
方法的返回值,wrapper
上挂载有很多的属性,比如text()
,find()
,findAll()
,props()
describe('HelloWorld.vue', () => {
it("渲染helloworld组件", ()=>{
const msg = "lalala demaxiya"
const wrapper = shallowMount(HelloWorld,{
propsData:{msg}
})
expect(wrapper.findAll('h1').length).toBe(1)
wrapper.setProps({msg:"hello"});
expect(wrapper.props("msg")).toEqual("hello");
})
})
在组件中使用快照测试
describe('HelloWorld.vue', () => {
it("渲染helloworld组件", ()=>{
const msg = "lala demaxiya"
const wrapper = shallowMount(HelloWorld,{
propsData:{msg}
})
expect(wrapper).toMatchSnapshot();
})
})
运行测试,会在__test__
文件夹下生成快照文件夹__snapshots__
,在UI测试的时候使用快照测试,很清晰捕捉UI界面的变化。
案例TodoList
使用TDD模式进行Header组件开发
开发流程,先思考组件需要什么功能,细化功能,进行测试文件编写,通过每个测试用例去完善组件的编写
Header.test.js
import { shallowMount } from '@vue/test-utils'
import Header from '../../components/Header.vue'
import { createWrapper } from '../../../../utils/utilTest.js';
describe('Header组件', () => {
it('快照测试,样式等发生变化给与提示', () => {
const wrapper = shallowMount(Header)
expect(wrapper).toMatchSnapshot();
})
it('必须含有input组件', () => {
const wrapper = shallowMount(Header)
const input = createWrapper(wrapper, 'input')
// wrapper.find("[data-input = 'input']")
expect(input.exists()).toBeTruthy()
})
it('input的value默认必须为空 ', () => {
const wrapper = shallowMount(Header)
const inputValue = wrapper.vm.inputValue
expect(inputValue).toBe('')
})
it('input的value发生变化时,对应的inputValue应该发生变化', () => {
const wrapper = shallowMount(Header)
const input = createWrapper(wrapper, 'input')
input.setValue('lalala')
expect(wrapper.vm.inputValue).toBe('lalala')
})
it('input按enter触发事件时,当无数据时不应该对外emit事件', () => {
const wrapper = shallowMount(Header)
const input = createWrapper(wrapper, 'input')
input.setValue('')
input.trigger('keyup.enter')
expect(wrapper.emitted().add).toBeFalsy()
})
it('input按enter触发事件,当有数据时对外触发emit事件,并且清空input中的value', () => {
const wrapper = shallowMount(Header)
const input = createWrapper(wrapper, 'input')
input.setValue('hello')
input.trigger('keyup.enter')
expect(wrapper.emitted().add).toBeTruthy()
expect(wrapper.vm.inputValue).toBe('')
})
})
注意api使用来自于官网:https://vue-test-utils.vuejs.org/zh/api/wrapper/#%E5%B1%9E%E6%80%A7
wrapper.find(selector)
表示查找某个元素,参数为css选择器。
input.exists()
判断一个元素是否存在。
wrapper.vm.inputValue
表示wrapper的Vue实例上的属性和方法。
input.setValue()
表示给input元素设置一个value置。
input.trigger('keyup.enter');
表示触发一个元素上的某个事件。
wrapper.emitted().add
表示wrapper实例上add方法被触发。
Header组件:
TodoList
undoList.test.js
import { shallowMount } from '@vue/test-utils'
import UndoList from '../../components/UndoList.vue'
import { createWrapper } from '../../../../utils/utilTest.js';
describe("UdoList组件", () => {
it('当数据为空时,展示undo的item的length为0,显示未完成数为0', () => {
const wrapper = shallowMount(UndoList, {
propsData: {
list: []
}
})
const undoItem = createWrapper(wrapper, 'item')
const undoCount = createWrapper(wrapper, 'count')
expect(undoItem.length).toEqual(0);
expect(undoCount.at(0).text()).toEqual("0")
})
it("有数据时[1,2,3],count中内容为3,且列表有内容,且存在删除按钮", () => {
const wrapper = shallowMount(UndoList, {
propsData: {
list: [
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"}
]
}
})
const undoItem = createWrapper(wrapper, 'item')
const undoCount = createWrapper(wrapper, 'count')
const undoDelete = createWrapper(wrapper, 'delete')
expect(undoItem.length).toEqual(3);
expect(undoCount.at(0).text()).toEqual("3")
expect(undoDelete.length).toEqual(3)
})
it("有数据时,点击删除按钮会向外触发一个事件", () => {
const wrapper = shallowMount(UndoList, {
propsData: {
list: [
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"}
]
}
})
const undoDeleteButton = createWrapper(wrapper, 'delete').at(2)
undoDeleteButton.trigger("click")
expect(wrapper.emitted().delete).toBeTruthy();
expect(wrapper.emitted().delete[0][0]).toBe(2)
})
it("点击item项会响外抛出一个事件changeType", () => {
const wrapper = shallowMount(UndoList, {
propsData: {
list: [
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"}
]
}
})
const undoDeleteButton = createWrapper(wrapper, 'item').at(2)
undoDeleteButton.trigger("click")
expect(wrapper.emitted().changeType).toBeTruthy()
expect(wrapper.emitted().changeType[0][0]).toBe(2)
})
it("当undoList数据中有type值为input的,页面中应该有一个input标签,标签内容为当前内容",() => {
const wrapper = shallowMount(UndoList, {
propsData:{
list:[
{value:1,type:"div"},
{value:2,type:"input"},
{value:3,type:"div"}
]
}
})
const input = createWrapper(wrapper,'input')
const inputValue = input.at(0).element.value
expect(input.length).toBe(1)
expect(inputValue).toBe("2")
})
it("列表项失去焦点,向外触发reset事件", () => {
const wrapper = shallowMount(UndoList, {
propsData: {
list: [
{value:1,type:"div"},
{value:2,type:"input"},
{value:3,type:"div"}
]
}
})
const inputElem = createWrapper(wrapper, 'input').at(0)
inputElem.trigger("blur")
expect(wrapper.emitted().reset).toBeTruthy()
})
it("列表input项发生change事件,向外触发change事件", () => {
const wrapper = shallowMount(UndoList, {
propsData: {
list: [
{value:1,type:"div"},
{value:123,type:"input"},
{value:3,type:"div"}
]
}
})
const inputElem = createWrapper(wrapper, 'input').at(0)
inputElem.trigger("change")
expect(wrapper.emitted().change).toBeTruthy()
expect(wrapper.emitted().change[0][0]).toEqual(
{
value:"123",
index:1
}
)
})
})
undoList.vue
正在进行
{{list.length}}
{changeValue(e.target.value,index)}"
/>
{{ item.value }}
-
TodoList.test.js
import { shallowMount } from '@vue/test-utils'
import TodoList from '../../TodoList.vue'
import Header from '../../components/Header.vue'
import UndoList from '../../components/UndoList.vue';
describe("TodoList组件", () => {
it('有一个存放未完成任务的数组undoList默认为空', () => {
const wrapper = shallowMount(TodoList)
const undoList = wrapper.vm.undoList
expect(undoList.length).toBe(0)
})
it('组件执行接收Header组件中传递的值,那么undoList就增加一项', () => {
/** 集成测试
const value = 'lisa'
const wrapper = shallowMount(TodoList)
const header = shallowMount(Header)
header.vm.$emit('add', value)
wrapper.vm.addItem(value)
expect(wrapper.vm.undoList).toEqual([value])
*/
//单元测试
const wrapper = shallowMount(TodoList)
wrapper.setData({
undoList: [
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"}
]
})
wrapper.vm.addItem(4);
expect(wrapper.vm.undoList).toEqual([
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"},
{value:4,type:"div"}
])
})
it("调用UndoList组件,应该传递一个list的props", () => {
const wrapper = shallowMount(TodoList)
const undoWrapper = wrapper.findComponent(UndoList)
let undoList = undoWrapper.props('list')
expect(undoList).toBeTruthy();
})
it("点击UndoList中删除时 TodoList中需要删除指定id项", () => {
const wrapper = shallowMount(TodoList)
wrapper.setData({
undoList: [
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"}
]
})
wrapper.vm.deleteUdoList(1)
expect(wrapper.vm.undoList).toEqual([
{value:1,type:"div"},
{value:3,type:"div"}
])
})
it("接收changeType事件然后修改TodiList中需要修改的指定项",() => {
const wrapper = shallowMount(TodoList)
wrapper.setData({
undoList: [
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"}
]
})
wrapper.vm.updateUndoList(1)
expect(wrapper.vm.undoList).toEqual([
{value:1,type:"div"},
{value:2,type:"input"},
{value:3,type:"div"}
])
})
it("接收reset事件,对数据进行修改",() => {
const wrapper = shallowMount(TodoList)
wrapper.setData({
undoList: [
{value:1,type:"div"},
{value:2,type:"input"},
{value:3,type:"div"}
]
})
wrapper.vm.resetType(1)
expect(wrapper.vm.undoList).toEqual([
{value:1,type:"div"},
{value:2,type:"div"},
{value:3,type:"div"}
])
})
it('接收change事件对数据进行修改', () => {
const wrapper = shallowMount(TodoList)
wrapper.setData({
undoList: [
{value:1,type:"div"},
{value:2,type:"input"},
{value:3,type:"div"}
]
})
wrapper.vm.changeValue({value:"456",index:1})
expect(wrapper.vm.undoList).toEqual([
{value:1,type:"div"},
{value:"456",type:"div"},
{value:3,type:"div"}
])
})
})
第二个测试用例表示Header组件触发一个事件add,在TodoList组件中随即触发一个addItem事件,然后去判断undoList数组。
TodoList.vue
测试代码覆盖率:在jest.config.js
中进行配置
collectCoverageFrom: ["**/*.vue", "!**/node_modules/**"]
该配置项表示,查找以.vue
结尾的文件并进行代码覆盖率测试。
在package.json
中进行配置:
"scripts":{
"test:cov": "vue-cli-service test:unit --coverage"
}
运行npm run test:cov
,会在项目根目录下生成coverage
文件夹,里面包含了测试覆盖率信息。控制台也有简要信息提示。
总结
TDD: Test Driven Development(测试驱动开发)
是一种模式,在写代码前进行思考,有助于代码质量提高。
单元测试
针对某个点进行测试,优点是针对一个组件或模块进行测试,测试覆盖率高,缺点业务耦合度较高,修改业务,修改测试代码,代码量大。过于独立,成功测试的单元,组合在一起不能保证正常运行。
适用场景:函数库