Vue Test Utils 测试技巧全指南
vue-test-utils Component Test Utils for Vue 2 项目地址: https://gitcode.com/gh_mirrors/vu/vue-test-utils
测试理念篇
测试什么:关注公共接口而非实现细节
在测试 Vue 组件时,我们不应该追求逐行覆盖的测试方式。这种测试方式会导致过度关注组件内部实现细节,最终产生脆弱的测试用例。
更合理的做法是将组件视为黑盒,专注于测试其公共接口。具体来说,每个测试用例应该验证:
- 给定某种输入(用户交互或 props 变化)
- 组件是否产生预期的输出(渲染结果或自定义事件)
示例分析:假设有一个计数器组件,每次点击按钮时显示值加1。正确的测试方式是:
- 模拟点击操作
- 断言渲染结果是否增加了1
- 不关心内部如何实现计数功能
这种方法的优势在于,只要组件公共接口保持不变,无论内部实现如何变化,测试都能保持通过。
浅渲染(shallowMount)的适用场景
当组件包含大量子组件时,完整渲染整个组件树可能会导致测试变慢或过于复杂。这时可以使用 shallowMount
方法进行浅渲染:
import { shallowMount } from '@vue/test-utils'
import Component from '../Component.vue'
const wrapper = shallowMount(Component)
浅渲染的特点:
- 只渲染当前组件
- 自动将子组件替换为存根(stub)
- 生成包含已挂载Vue组件的Wrapper对象
使用建议:浅渲染会使被测组件与实际运行时的组件行为有所差异(部分内容不会被渲染),因此除非遇到性能问题或需要简化测试准备,否则建议优先使用完整挂载(mount)。
生命周期与异步测试
生命周期钩子的测试要点
使用 mount
或 shallowMount
时,组件会响应所有生命周期事件,但需要注意:
beforeDestroy
和destroyed
钩子只有在手动调用Wrapper.destroy()
时才会触发- 组件不会在每个测试用例结束时自动销毁
- 需要手动清理可能继续运行的任务(如
setInterval
或setTimeout
)
异步测试最佳实践
由于 Vue 的更新机制是异步的(在下一个"tick"执行),测试时必须等待更新完成。有两种处理方式:
async/await 方式(推荐):
it('updates text', async () => {
const wrapper = mount(Component)
await wrapper.trigger('click')
expect(wrapper.text()).toContain('updated')
})
Promise 链方式:
it('render text', done => {
const wrapper = mount(TestComponent)
wrapper.trigger('click').then(() => {
expect(wrapper.text()).toContain('updated')
done()
})
})
事件测试技巧
自定义事件断言
Wrapper 会自动记录组件实例触发的所有事件,可以通过 wrapper.emitted()
获取:
wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)
// 断言事件已触发
expect(wrapper.emitted().foo).toBeTruthy()
// 断言事件触发次数
expect(wrapper.emitted().foo.length).toBe(2)
// 断言事件负载
expect(wrapper.emitted().foo[1]).toEqual([123])
子组件事件触发测试
测试父组件对子组件事件的响应:
describe('ParentComponent', () => {
it("displays 'Emitted!' when custom event is emitted", () => {
const wrapper = mount(ParentComponent)
wrapper.findComponent(ChildComponent).vm.$emit('custom')
expect(wrapper.html()).toContain('Emitted!')
})
})
状态与属性管理
组件状态操作
可以直接修改组件状态进行测试:
it('manipulates state', async () => {
await wrapper.setData({ count: 10 }) // 修改数据
await wrapper.setProps({ foo: 'bar' }) // 修改props
})
Props 模拟
有多种方式模拟 props:
挂载时传入:
mount(Component, {
propsData: {
aProp: 'some value'
}
})
挂载后更新:
wrapper.setProps({ aProp: 'new value' })
高级测试场景
过渡动画(Transition)测试
测试包含 <transition>
的组件时,由于 Vue 的实现机制,直接测试可能存在问题。推荐两种解决方案:
方案1:使用过渡存根:
const transitionStub = () => ({
render: function(h) {
return this.$options._renderChildren
}
})
mount(Foo, {
stubs: {
transition: transitionStub()
}
})
方案2:避免 setData,拆分测试:
// 测试显示状态
test('should render Foo', () => {
const wrapper = mount(Foo, {
data() { return { show: true } }
})
expect(wrapper.text()).toMatch(/Foo/)
})
// 测试隐藏状态
test('should not render Foo', () => {
const wrapper = mount(Foo, {
data() { return { show: false } }
})
expect(wrapper.text()).not.toMatch(/Foo/)
})
全局插件和混入测试
对于依赖全局插件(如 vuex、vue-router)的组件,可以使用 createLocalVue
实现隔离测试:
import { createLocalVue, mount } from '@vue/test-utils'
const localVue = createLocalVue()
localVue.use(MyPlugin)
mount(Component, {
localVue
})
注意:某些插件(如 Vue Router)会向 Vue 构造函数添加只读属性,这种情况下无法在 localVue 上重新安装插件或添加模拟。
依赖注入模拟
可以使用 mocks
选项模拟注入的属性:
mount(Component, {
mocks: {
$route: {
path: '/',
params: { id: '123' }
}
}
})
组件存根
通过 stubs
选项可以替换全局或局部注册的组件:
mount(Component, {
stubs: ['globally-registered-component'] // 替换为空白存根
})
样式测试说明
需要注意的是,测试只能在 jsdom 环境下检测到内联样式,无法检测通过 CSS 类应用的样式。
vue-test-utils Component Test Utils for Vue 2 项目地址: https://gitcode.com/gh_mirrors/vu/vue-test-utils
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考