Skip to content

测试 vue 组件

我们用 @vue/test-utils 来测试 vue 组件。

下载三个依赖

shell
# 用来模拟浏览器环境
pnpm i jsdom -D
# 用来解析 vue 单文件组件
pnpm i @vitejs/plugin-vue -D
# 用来生成 vue 组件实例
pnpm i @vue/test-utils -D

配置插件

vitest.config.ts 中:

ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()], 
  test: {
    /**
     * 配置环境
     *
     * @see environment https://cn.vitest.dev/config/#environment
     *
     * 这里建议使用 jsdom 因为 happy-dom 会有一些不可预期错误,详情参考:
     *
     * @see test-utils https://github.com/vuejs/test-utils/issues/1704
     * @see test-utils https://github.com/vuejs/test-utils/issues/1602
     * @see fighting-design https://github.com/FightingDesign/fighting-design/pull/346
     */
    environment: 'jsdom', 
  },
})

断定组件的根标签包含有某个类名

vue 组件:

vue
<template>
  <div class="text-red">
    <div class="ma-10">asdf</div>
  </div>
</template>

mount 方法用来挂载 vue 单文件组件,并返回一个 wrapper 对象。wrapper 对象上有一些方法和属性。

wrapper.classes('text-red') 传入某个具体的类名,会判断该类名是否存在,返回布尔值。

wrapper.classes() 方法传入具体的类型,返回所有由类名组成的数组。

js
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'

it('测试 vue 组件', () => {
  const wrapper = mount(testVueComp)
  expect(wrapper.classes('text-red')).toBe(true)
  expect(wrapper.classes()).toContain('text-red')
})

获取组件的某个节点,并断定它还有其它类名

vue 模板:

vue
<template>
  <div class="text-red">
    <div class="abc def">asdf</div>
  </div>
</template>

wrapper.get 方法返回匹配选择器的第一个 dom 节点或 vue 组件的 wrapper。

ts
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'

it('测试 vue 组件', () => {
  const wrapper = mount(testVueComp)
  expect(wrapper.get('.abc').classes('def')).toBe(true)
})

给组件传入 prop

js
  const wrapper = mount(testVueComp, {
    props: {
      nickName: '张三'
    }
  })

断言标签内的文本内容

vue 组件:

vue
<template>
  <div class="name">
    {{ nickName }}
  </div>
</template>
<script>
export default {
  props: {
    nickName: ''
  }
}
</script>

使用 wrapper.get('.name').text() 方法可以获得匹配选择器标签内部的文本内容。

js
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'

it('测试 vue 组件', () => {
  const wrapper = mount(testVueComp, {
    props: {
      nickName: '张三'
    }
  })
  expect(wrapper.get('.name').text()).toBe('张三')
})

获取某个节点的 attributes 属性对象

vue 组件:

vue
<template>
  <div :info="info" :isShow="isShow" b c>
  </div>
</template>
<script>
export default {
  props: ['info'],
  data() {
    return {
      isShow: "true"
    }
  }
}
</script>

使用 wrapper.attributes() 方法,可以获取到匹配选择器的所有 attributes。注意不包括父组件传入进来的 prop 属性,以及组件的 data 数据。

这里注意,虽然打印出的 attributes() 包含了 data 中的 isShow。但是如果打印 attributes('isShow'),结果为 undefined。如果想要断言 prop、data,可以从 wrapper.vm 对象上获取并进行断言。

ts
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'

it('测试 vue 组件', () => {
  const wrapper = mount(testVueComp, {
    info: '123'
  })
  console.log(wrapper.attributes()) 
  expect(wrapper.attributes())
})

高亮代码行的打印部分如下。可以看到并不包含 prop。

也由此知道,如果省略给 attribute 赋值,那么 attribute 默认为空串。

js
{ isshow: 'true', b: '', c: '' }

断言节点存在某个属性

使用 wrapper.attribute('.selector') 方法。

vue
<template>
  <div b c>
  </div>
</template>

如果省略给 attribute 赋值,那么 attribute 默认为空串。

js
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'

it('测试 vue 组件', () => {
  const wrapper = mount(testVueComp)
  expect(wrapper.attributes('b')).toBe('')
})

点击某个元素后,断言某个响应式数据会变化

组件:

vue
<template>
  <div class="button" @click="add">
    {{ msg }}
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: ''
    }
  },
  methods: {
    add() {
      this.msg = 123
    }
  }
}
</script>

我们想要模拟点击 class 为 button 的 div 标签。其实也就是让其触发 click 点击事件。可以使用 trigger 方法。

再前面加上 await,确保组件已经被更新。

js
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'

it('测试 vue 组件', async () => {
  const wrapper = mount(testVueComp)
  await wrapper.trigger('click') 
  expect(wrapper.text()).toBe('123')
  // 也可以直接断言组件的 data 数据。相当于断言 this.msg。 
  expect(wrapper.vm.msg).toBe(123)
})

断言组件向外触发了某个事件

总结

经过测试,发现:

如果在组件根标签上定义 click 点击事件,那么无论是否加修饰符 .stopwrapper.emitted() 都会包含 click 事件。

如果不是在组件根标签上定义 click 点击事件,那么加了修饰符 .stop 会导致 wrapper.emitted() 不会包含 click 事件。

组件可以触发自定义事件,也可以触发原生事件(比如点击事件)。注意原生事件默认是会向上冒泡的。

比如我有下面这样一个组件。当我点击按钮时,会触发几个事件呢?正确答案是两个:一个原生冒泡事件 click,一个自定义事件 sayHello。

vue
<template>
  <div>
    <button @click="add">按钮</button>
  </div>
</template>
<script>
export default {
  methods: {
    add() {
      this.$emit('sayHello', 123)
    }
  }
}
</script>

我们使用 wrapper.emitted() 方法来断言:

js
import { it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import testVueComp from './test-vue-comp.vue'

it('测试 vue 组件', async () => {
  const wrapper = mount(testVueComp)
  await wrapper.find('button').trigger('click')
  console.log(wrapper.emitted()) 
})

console.log(wrapper.emitted()) 打印如下。对象的 key 是对外触发的事件名,对象的 value 是一个数组,每触发一次该事件,都会往这个数组里推入触发该事件的入参(类型也为数组)。

js
{ 
  sayHello: [ [ 123 ] ], 
  click: [ [ [MouseEvent] ] ] 
}

除此之外,还可以往 wrapper.emitted() 里传入参数,参数的含义与事件触发对象的 key 相同。

wrapper.find() 与 wrapper.get() 区别

find 如果找不到匹配选择器的元素,什么都不会做。

get 如果找不到匹配选择器的元素,会抛出异常。

断言默认插槽

js
const wrapper = mount(button, {
  slots: {
    // 默认插槽
    default: 'Hello world'
  }
})

Released under the MIT License.