前端自动化测试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: {
    '^@/(.*)$': '/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组件:






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







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

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

单元测试

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

适用场景:函数库

你可能感兴趣的:(前端自动化测试TDD与单元测试学习记录)