vue3单元测试

371 阅读2分钟

Vue单元测试

用到的库

1.  vitest 是 Vitest 测试工具核心模块,提供了单元测试管理和断言等工具

2.  @vue/test-utils 是 Vue.js 测试工具,辅助处理 Vue.js 在 Node.js 环境里操作 DOM 渲染和 DOM 事件等操作

3.  vitejs/plugin-vue 是 vite 的插件,支持直接构建运行 Vue.js sfc代码

4.  jsdom 用来在 Node.js 环境中模拟浏览器的原生 API,例如操作 DOM 的原生 API 等

安装:

npm i -D vitest @vue/test-utils @vitejs/plugin-vue @vitejs/plugin-vue jsdom

配置

创建./vitest.config.ts,写入以下内容:

import { defineConfig } from 'vitest/config';
import PluginVue from '@vitejs/plugin-vue';
export default defineConfig({
  // 配置插件,用来在测试过程中编译Vue.js的模板语法
  plugins: [PluginVue()],
  // 配置测试环境,支持全局变量和浏览器DOM API
  test: {
    globals: true,
    environment: 'jsdom'
  }
});

./package.json

{
  // ...
  "scripts": {
    // ...
    "ui:test": "vitest",
    "ui:test:update": "vitest --update",
  }
  // ...
}

测试覆盖率

安装:

npm i -D @vitest/coverage-v8

配置:

/vitest.config.ts

import { defineConfig } from 'vitest/config';
import PluginVue from '@vitejs/plugin-vue';
export default defineConfig({
  // 配置插件,用来在测试过程中编译Vue.js的模板语法
  plugins: [PluginVue()],
  // 配置测试环境,支持全局变量和浏览器DOM API
  test: {
    globalstrue,
    environment'jsdom',
    coverage: {
      // 覆盖率统计工具
      provider'v8',
      // 覆盖率的分母,packages/ 目录里
      include: ['packages/components/**/src/*'],
      exclude: [],
      // 全量覆盖率计算
      alltrue,
    },
  }
});

./package.json

{
  // ...
  "scripts": {
    // ...
    "ui:test:coverage": "vitest run --coverage",
  }
  // ...
}

运行npm run ui:test:coverage后根目录下会出现coverage文件夹,进去后点index.html,就会出现覆盖率分析图

test.png

指标说明
Statements语句覆盖率:是不是每个语句都执行了?
Branches分支覆盖率:是不是每个if代码块都执行了?
Functions函数覆盖率:是不是每个函数都调用了?
Lines行覆盖率:是不是每一行都执行了?

点进具体文件就可以看到哪些代码没有覆盖到,可以针对性进行测试 image (4).png

常用的API介绍

@vue/test-utils

test-utils.vuejs.org/api/

mount: 挂在组件

import { mount } from '@vue/test-utils'
test('Test visibleChange', async () => {
    const wrapper = mount(Tooltip, {
      props: { title: 'Tooltip Title' },
    })
    if (wrapper.vm.onVisibleChange) {
      wrapper.vm.onVisibleChange(true)
    }
    await nextTick()
    // 判断emit中有没有visibleChange
    expect(wrapper.emitted()).toHaveProperty('visibleChange')
  })

上面得到的wrapper常用的方法:

wrapper.setProps({ toastMode: true })
// 拿到这个Dom
wrapper.find('.ath-toast')
// 判断存不存在
wrapper.get('.ath-toast')
// 获得输出属性
wrapper.emitted()
// 执行行为
wrapper.trigger('click')
wrapper.trigger('mouseenter')
// 获取组件实例
wrapper.vm
// 获取HTML
wrapper.html()

vitest

cn.vitest.dev/guide/

如果有更新DOM的操作,必须在断言之前执行:await nextTick()

wrapper.find('button').tragger('click')
await nextTick()

含setTimeout()等计时器的代码

test('Test closeToastTooltip', async () => {
      // 开启计时器环境
      vi.useFakeTimers()
      // 执行方法
      ...
      // 执行及时器,不加的话含有setTimeout 之类计时器相关的代码不会被测到
      vi.runAllTimers()
      expect(true)
    })

遇到的报错以及修改方法

1、  TypeError: window.matchMedia is not a function

解决方式:

加入以下代码,模拟window.matchMedia

import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, test, expect, vi } from 'vitest'
import Descriptions from '../index'

describe('Descriptions', () => {
  test('render', async () => {
  
    // 解决window.matchMedia报错
    Object.defineProperty(window, 'matchMedia', {
      writable: true,
      value: vi.fn().mockImplementation((query) => ({
        matches: false,
        media: query,
        onchange: null,
        addListener: vi.fn(), // Deprecated
        removeListener: vi.fn(), // Deprecated
        addEventListener: vi.fn(),
        removeEventListener: vi.fn(),
        dispatchEvent: vi.fn(),
      })),
    })
    
    const wrapper = mount(Descriptions)
    await nextTick()
    expect(wrapper.html()).toMatchSnapshot()
  })
})