Vue Test Utils 测试技巧全指南

Vue Test Utils 测试技巧全指南

vue-test-utils Component Test Utils for Vue 2 vue-test-utils 项目地址: https://gitcode.com/gh_mirrors/vu/vue-test-utils

测试理念篇

测试什么:关注公共接口而非实现细节

在测试 Vue 组件时,我们不应该追求逐行覆盖的测试方式。这种测试方式会导致过度关注组件内部实现细节,最终产生脆弱的测试用例。

更合理的做法是将组件视为黑盒,专注于测试其公共接口。具体来说,每个测试用例应该验证:

  1. 给定某种输入(用户交互或 props 变化)
  2. 组件是否产生预期的输出(渲染结果或自定义事件)

示例分析:假设有一个计数器组件,每次点击按钮时显示值加1。正确的测试方式是:

  • 模拟点击操作
  • 断言渲染结果是否增加了1
  • 不关心内部如何实现计数功能

这种方法的优势在于,只要组件公共接口保持不变,无论内部实现如何变化,测试都能保持通过。

浅渲染(shallowMount)的适用场景

当组件包含大量子组件时,完整渲染整个组件树可能会导致测试变慢或过于复杂。这时可以使用 shallowMount 方法进行浅渲染:

import { shallowMount } from '@vue/test-utils'
import Component from '../Component.vue'

const wrapper = shallowMount(Component)

浅渲染的特点:

  • 只渲染当前组件
  • 自动将子组件替换为存根(stub)
  • 生成包含已挂载Vue组件的Wrapper对象

使用建议:浅渲染会使被测组件与实际运行时的组件行为有所差异(部分内容不会被渲染),因此除非遇到性能问题或需要简化测试准备,否则建议优先使用完整挂载(mount)。

生命周期与异步测试

生命周期钩子的测试要点

使用 mountshallowMount 时,组件会响应所有生命周期事件,但需要注意:

  1. beforeDestroydestroyed 钩子只有在手动调用 Wrapper.destroy() 时才会触发
  2. 组件不会在每个测试用例结束时自动销毁
  3. 需要手动清理可能继续运行的任务(如 setIntervalsetTimeout

异步测试最佳实践

由于 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 vue-test-utils 项目地址: https://gitcode.com/gh_mirrors/vu/vue-test-utils

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

裴进众Serene

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值