vue2 单元测试

437 阅读3分钟

1.安装

安装方式1

  1. 如果正常执行的话,修改 .eslintrc.js,package.json(增加test:unit命令),增加tests文件夹(units/example.spec.js)
  2. 经常安装不成功,导致执行出错,提示render不存在等报错,那就采用第二种方式安装
vue add unit-jest

安装方式2

  1. 把下面的内容粘贴到package.json,执行yarn
"@vue/cli-plugin-unit-jest": "^4.5.17",
"@vue/server-test-utils": "^1.3.0",
"@vue/test-utils": "^1.0.3",
"vue-server-renderer": "2.6.12",
  1. 修改.eslintrc.js,增加overrides,与rules同级,其实增加单元测试eslint代码测试
overrides: [
  {
    files: [
      '**/__tests__/*.{j,t}s?(x)',
      '**/tests/unit/**/*.spec.{j,t}s?(x)'
    ],
    env: {
      jest: true
    }
  }
]
  1. 这里更推荐在测试组件所在目录建__tests__,然后新建xxx.spec.js 如测试Hello.vue,我们就在同级目录创建__tests__,然后创建Hello.spec.js,有时候为了方法查找,执行单个单元测试,名字需唯一,则加入父文件夹名称,如parent_Hello.spec.js,yarn test:unit parent_Hello.spec.js,就可以执行单个单元测试了

2.测试用例,官方文档

  1. 断言jest,官方网站
  2. 组件挂载@vue/test-utils,官方网站

写测试用例

  1. 测试用例建议写在组件同级目录,就近维护,建__tests__文件夹存放相关测试用例
  2. jest.config.js 更多配置
    • transformIgnorePatterns 必须配置,默认直接忽略node_modules的所有import,负责导致ant-design-vue等import报错
module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  // yarn test 是否生产报告,false则表示yarn testc才会生成报告
  collectCoverage: false,
  // 测试报告编译覆盖的文件
  collectCoverageFrom: [
    'src/components/customizeFormItem/*.{vue,jsx}',
    'src/components/table/.{vue,jsx}',
    'src/components/tableBatch/.{vue,jsx}',
    'src/components/.{vue,jsx}',
    'src/views/tools/**/*.{vue,jsx}',
    'src/views/user/**/*.{vue,jsx}',
    'src/views/wx/**/*.{vue,jsx}',
    'src/utils/*.js',
    'src/store/*.js'
  ],
  // 模块名称对应的地址map(加快编译速度)
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^ant-design-vue$': '<rootDir>/node_modules/ant-design-vue/es/index.js',
    '^vue$': '<rootDir>/node_modules/vue/dist/vue.common.js',
    '^vxe-table$': '<rootDir>/node_modules/vxe-table/lib/index.common.js'
  },
  // 不解析node_modules的包,不包含()的内容
  transformIgnorePatterns: ['node_modules/(?!(ant-design-vue|vxe-table|vue|element-ui)/)'],
  // 启动页
  setupFiles: [
    '<rootDir>/tests/setup.js'
  ]
}
  1. 测试用例:vue组件
  • input类型
   import { mount } from '@vue/test-utils'
   import InputNumber from '@/components/customizeFormItem/InputNumber'
   import { InputNumber as AntInputNumber } from 'ant-design-vue'

   describe('InputNumber.vue', () => {
     let wrapper
     beforeEach(() => {
       wrapper = mount(InputNumber, {
         listeners: {
           change(val) {
             wrapper.setProps({
               value: val
             })
           }
         },
         attrs: {
           addonBefore: '美元',
           addonAfter: '$'
         }
       })
     })

     it('检测slot:addonBefore和addonAfter', async () => {
       expect(wrapper.text()).toMatch('美元$')
       expect(wrapper.findAllComponents(AntInputNumber)).toHaveLength(1)
     })

     it('模拟人工输入', async () => {
       await wrapper.find('input').setValue('1000')
       expect(wrapper.props().value).toBe(1000)
       expect(wrapper.vm.localValue).toBe(1000)
     })

     afterEach(() => {
       wrapper.destroy()
     })
   })
  • 普通组件
import { mount } from '@vue/test-utils'
import EcSelect from '@/components/customizeFormItem/EcSelect.vue'

const stubs = [
  'vxe-list',
]
describe('EcSelect.vue', () => {
  let wrapper
  beforeEach(() => {
    wrapper = mount(EcSelect, {
      stubs,
      propsData: {
        options: [
          { label: '张三', value: '1' },
          { label: '李四', value: '2' },
          { label: '王五', value: '3' },
          { label: '赵六', value: '4' }
        ],
        multiple: true,
        value: ['1', '2']
      },
      data() {
        return {
          visible: true
        }
      },
      listeners: {
        input: function (val) {
          wrapper.setProps({ value: val })
        }
      }
    })
  })

  it('测试单选-默认值是否选中', async () => {
    expect(wrapper.vm.selectLabel).toEqual(['张三', '李四'])
  })

  afterEach(() => {
    wrapper.destroy()
  })
})
  • 函数测试用例:测试函数和vuex(mutations, acitons里面的方法)
import {
  merge,
  deepClone
} from '../utils'

it('merge', () => {
  const obj1 = { a: 1, b: 1 }
  const obj2 = { a: 2, children: [{ a: 1 }], c: 2 }
  const obj3 = { a: 3, children: [{ a: 3 }, { b: 3 }], d: 3 }
  const target = { a: 3, children: [{ a: 3 }, { b: 3 }], b: 1, c: 2, d: 3 }
  expect(merge(obj1, obj2, obj3)).toEqual(target)
})

describe('deepClone', () => {
  it('复制对象', () => {
    const obj = { a: 1, b: 1 }
    expect(deepClone(obj)).toEqual(obj)
  })
  it('复制数组', () => {
    const arr = [1, 2, 3, { a: 1 }]
    expect(deepClone(arr)).toEqual(arr)
  })
})

3. 经验

  1. shallowMount和mount
  • 两者如果没有直接在组件内部注册,执行单元测试,会提示组件没有注册
  • 如果组件有注册过,shallowMount会把当前组件components没有覆盖的组件,自动转为xxx-stub,而mount会把所有的组件都渲染出来
  • mount比shallowMount执行时间长,占内存多
  • 正常shallowMount挂载,只有当你需要渲染整个组件的时候,采用mount,这也会导致你需要处理更多情况,比如子组件用了router,vuex等
  1. shallowMount和mount的配置项,更多配置项
wrapper = mount(EcSelect, {
  context: { // 将上下文传递给函数式组件。该选项只能用于函数组件,比较少用
    props: { show: true },
    children: [Foo, Bar]
  }
  stubs, // 组件占位符,['a-tag', 'a-button'] 或者 { 'a-tag': Tag, 'a-button': true }
  propsData: { // 等同于给组件传递props
    options: [
      { label: '张三', value: '1' },
      { label: '李四', value: '2' },
      { label: '王五', value: '3' },
      { label: '赵六', value: '4' }
    ],
    multiple: true,
    value: ['1', '2']
  },
  slots: { 插入slots ,组件内使用$slots获取
    baz: bazComponent, // 支持组件
    qux: '<my-component />随便写两字', // 支持字符串组件,但是需在stubs配置{ 'my-component': myComponent }
    default: [Foo, '<my-component />', 'text'] // 支持数组,字符串组件,但是需在stubs配置{ 'my-component': myComponent }
  },
  scopedSlots: {  // 插入scopedSlots,组件内使用$scopedSlots获取
    default: '<p>{{props.index}},{{props.text}}</p>', // 支持字符串
    foo(props) { // 也支持函数
      return <div>{props.text}</div>
    }
  },
  mocks: { // 虚拟组件内的全局方法
     $xxx: () => {}
  },
  data() { // 将会覆盖组件的data函数
    return {
      visible: true
    }
  },
  listeners: { // 监听组件事件,类似@change,或者jsx的on对象
    input: function (val) {
      wrapper.setProps({ value: val })
    }
  },
  router, // import router from '@/router'
  store, // import router from '@/store',
  localVue, // 可以localVue.use注入全局组件,但是不建议,配置jest.config.js>setupFiles更合适,在启动页注册全局组件
  provide() { // 注入provide
    return {
      $IndexCtx: {}
    }
  }
  ...其他选项
})
  1. 执行单个测试用例,yarn test:unit xxx.spec.js(全局目录搜索),或者 yarn test:unit xxx/xxx/xxx.spec.js(执行同目录的类似名字文件)),基本都能定位到当前组件,如果实在如法定位,就使用唯一命名
    • 监听并执行测试用例:yarn test:unit test xxx.spec.js --watch // 默认执行 jest -o 监视有改动的测试
  2. yarn test:unit --coverage 生成测试报告,
  3. 哪些页面可以写用测用例
    • 纯函数(utils/xxx.js)
    • vuex的mutations,actions
    • 非业务组件,业务组件需要调用接口,无法用模拟接口来测试接口
  4. beforeEach 和 afterEach,describe,expect,it, test... 是jest全局函数或者关键字,beforeEach 和 afterEach需在describe内部运行,三个函数都是非必须的
  5. 常用断言toBe(非引用类型, boolen, number, string) toMatch(字符串类型), toEqual(引用类型,array, object)
  6. mount配置项内的data函数,将会覆盖组件的data函数