前端自动化测试TDD与单元测试学习记录

https://vue-test-utils.vuejs.org/zh/

TDD (Test Driven Development 测试驱动开发)

TDD开发流程

  1. 编写测试用例。
  2. 运行测试,测试用例无法通过。
  3. 编写代码,使测试用例通过测试。
  4. 优化代码,完成开发。
  5. 重复上述步骤

TDD的优势

  1. 长期减少回归bug.
  2. 代码质量更好(组织,可维护性)。
  3. 测试覆盖率高。
  4. 错误测试代码不容易出现。

在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);
    })
})

shallowMountvue 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(测试驱动开发)

是一种模式,在写代码前进行思考,有助于代码质量提高。

单元测试

针对某个点进行测试,优点是针对一个组件或模块进行测试,测试覆盖率高,缺点业务耦合度较高,修改业务,修改测试代码,代码量大。过于独立,成功测试的单元,组合在一起不能保证正常运行。

适用场景:函数库

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259