用Vue Test Utils编写组件测试,这些边界情况你考虑了吗
用 Vue Test Utils 编写组件测试时,应重点覆盖异步等待、props 不传或传 null 与空值、emits 触发次数和参数、v-model 双向更新以及 slots、provide/inject 等边界,避免只等 nextTick 或用 toBeTruthy 判断,导致测试看似通过却未验证真实行为。
写组件测试的时候,最让人头疼的往往不是"跑不跑得通",而是那些看起来跑通了、实际上什么都没验证的测试。绿油油的覆盖率背后,可能藏着一堆"删掉断言也不会挂"的用例。用 Vue Test Utils 写测试,真正拉开水平差距的,是对边界情况的敏感度。
异步不只是 await nextTick
Vue 的更新是异步批处理的,这点大家都懂。但很多同学写了 await wrapper.find('button').trigger('click') 就直接断言 DOM,结果通过率忽高忽低。原因通常是组件内部还有一层异步——比如请求接口、watch 里调用了 async 方法。nextTick 只保证 Vue 的渲染队列被清空,不保证你组件里的 Promise 链走完了。
await wrapper.find('button').trigger('click')
await flushPromises() // 等组件内部所有 Promise 落地
expect(wrapper.text()).toContain('加载完成')
另外提醒一句:trigger 返回的 Promise 本身已经包含一次 nextTick,再手动 await nextTick() 往往是多余的。真正要警惕的是连续触发多次状态变更后只等了一次。
Props:不传、传 null、传空值
在 Vue 里,"不传"和"显式传 undefined"都会走 default,但传 null 不会。很多线上 bug 就藏在这儿。建议至少写三条用例覆盖:不传、传 null、传空数组或空字符串。
还有 props 的响应式更新:wrapper.setProps({ list: [] }) 之后不能立刻断言,同样需要 await。别忘了 props 更新可能连带触发 watch,进而触发事件——这时候断言的重点就变成了"触发了几次"。
emits 到底触发了几次
expect(wrapper.emitted('change')).toHaveLength(1)
比 toBeTruthy() 有用得多。组件因为 watch 加了 immediate: true,或者父子双方各调了一次,导致事件触发两次的情况非常常见,只判断"有没有"根本发现不了。emitted() 拿到的是每次触发的参数数组,断言参数比断言次数更接近真实行为。
v-model 要测"双向"
只测 modelValue 变化后 UI 更新,只覆盖了一半;另一半是用户输入后有没有 update:modelValue 派发出去。
await wrapper.find('input').setValue('hello')
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['hello'])
setValue 会自动触发 input 事件并 await,比手动 trigger('input') 更接近真实输入。
Slots、provide/inject 与全局注册
测试带插槽的组件时,别忘了 shallowMount 不会渲染子组件。你要断言插槽内容真的渲染出来了,就得用 mount,或者用 slots 显式传入测试桩。provide/inject 建议在测试里显式注入假对象,而不是依赖真实父组件,否则测试会变得极其脆弱——父组件一改,子组件测试全红。全局注册的组件(比如 UI 库)用 global.plugins 或 global.stubs 明确声明,别让"本地能跑、CI 挂掉"这种事故反复发生。
定时器与卸载清理
组件里写了 setInterval、addEventListener、ResizeObserver,测试结束时清理干净了吗?建议加一条用例:wrapper.unmount() 之后断言 clearInterval 被调用过。
const spy = vi.spyOn(global, 'clearInterval')
wrapper.unmount()
expect(spy).toHaveBeenCalled()
这类测试能提早暴露内存泄漏,尤其在反复挂载卸载的单页应用里。用 vi.useFakeTimers() 时要小心和 await 混用,假定时器会拦截 setTimeout,如果代码依赖它做异步调度,记得配合 vi.advanceTimersByTimeAsync()。
Teleport、Transition 和必须真实 DOM 的场景
Teleport 的内容不在 wrapper.html() 里,得去 document.body 找。Transition 在测试环境默认不执行过渡,"动画结束后"的状态往往要直接跳过。涉及 IntersectionObserver、matchMedia、scrollIntoView 的组件,这些 API 在 jsdom 里压根不存在,提前在 setup 文件里 mock 掉,比在每个用例里打补丁优雅得多。
结语
组件测试的价值从来不是覆盖率数字,而是它能不能在你重构时给出真实反馈。上面这些边界情况,绕来绕去都在回答同一个问题:我这条断言,到底验证了什么?如果把它删掉测试依然通过,那它大概率没在干活。写测试花的时间和写组件差不多,但省下来的调试时间,通常要多得多。
转载请注明出处,版权归原作者所有。
正式会员
认证极客






