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: {
'^@/(.*)$': '<rootDir>/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组件:
<template>
<div class="header">
<div class="header-content">
TodoList
<input type="text"
data-input="input"
v-model="inputValue"
@keyup.enter="changeInput"
placeholder="TodoItem"
/>
</div>
</div>
</template>
<script>
export default {
name: 'Header',
data: () => {
return {
inputValue: ''
}
},
methods: {
changeInput () {
if (this.inputValue) {
this.$emit('add', this.inputValue)
this.inputValue = ''
}
}
}
}
</script>
<style scoped lang="stylus">
.header{
line-height 60px;
background #333;
}
.header-content{
width:500px;
margin 0 auto;
font-size:24px;
color:#fff;
}
.header-content input{
float:right;
width:350px;
outline:none;
text-indent:10px;
margin-top:22px;
}
</style>
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
<template>
<div class="undolist">
<div class="count">
<span>正在进行</span>
<span class="countNum" data-input="count" >{{list.length}}</span>
</div>
<div class="itemsWrap">
<div v-for="(item,index) in list"
:key="index" class="items"
data-input="item"
@click="handleChangeType(index)" >
<input class="undoinput"
v-if="item.type == 'input' "
data-input='input' type="text"
:value="item.value"
@blur="resetType"
@change="(e) => {changeValue(e.target.value,index)}"
/>
<span v-else>{{ item.value }}</span>
<span class="delete"
data-input="delete"
@click="handleClick(index)" >
-
</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'UndoList',
props: ['list'],
methods: {
handleClick (index) {
this.$emit('delete', index)
},
handleChangeType (index) {
this.$emit('changeType', index)
},
resetType () {
this.$emit('reset')
},
changeValue (value, index) {
this.$emit('change', {
value,
index
})
}
}
}
</script>
<style scoped>
.count{
width: 500px;
margin: 0 auto;
font-size: 20px;
margin-bottom: 10px;
}
.countNum{
float: right;
font-size: 16px;
width: 20px;
height: 20px;
background: #333;
color: #fff;
line-height: 20px;
text-align: center;
margin-top: 5px;
border-radius: 50%;
}
.itemsWrap{
width: 500px;
margin: 0 auto;
font-size: 16px;
}
.items{
background: #fff;
border-left: 5px solid cyan;
border-radius: 3px;
text-indent: 5px;
padding: 3px 0;
margin-bottom: 8px;
}
.delete{
float: right;
font-size: 16px;
width: 20px;
height: 20px;
background: #999;
color: #fff;
line-height: 20px;
text-align: center;
border-radius: 50%;
text-indent: 0;
cursor: pointer;
}
.undoinput{
width: 300px;
height: 16px;
text-indent: 4px;
}
</style>
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
<template>
<div>
<Header @add='addItem' />
<undoList
:list="undoList"
@delete='deleteUdoList'
@changeType='updateUndoList'
@reset='resetType'
@change="changeValue"/>
</div>
</template>
<script>
import Header from './components/Header.vue'
import undoList from './components/UndoList.vue'
export default {
name: 'HelloWorld',
props: {
msg: String
},
data: () => {
return {
undoList: []
}
},
components: {
Header,
undoList
},
methods: {
addItem (value) {
this.undoList.push({
value: value,
type: 'div'
})
},
deleteUdoList (index) {
this.undoList.splice(index, 1)
},
updateUndoList (index) {
const newArr = []
this.undoList.forEach((item, itemIndex) => {
if (itemIndex === index) {
newArr.push({ value: item.value, type: 'input' })
} else {
newArr.push({ value: item.value, type: 'div' })
}
})
this.undoList = [...newArr]
},
resetType () {
const newArr = []
this.undoList.forEach((item, itemIndex) => {
newArr.push({ value: item.value, type: 'div' })
})
this.undoList = [...newArr]
},
changeValue (dataset) {
this.undoList[dataset.index].value = dataset.value
this.undoList[dataset.index].type = 'div'
}
}
}
</script>
<style scoped lang="stylus"></style>
测试代码覆盖率:在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(测试驱动开发)
是一种模式,在写代码前进行思考,有助于代码质量提高。
单元测试
针对某个点进行测试,优点是针对一个组件或模块进行测试,测试覆盖率高,缺点业务耦合度较高,修改业务,修改测试代码,代码量大。过于独立,成功测试的单元,组合在一起不能保证正常运行。
适用场景:函数库