用Vue Test Utils编写组件测试,这些边界情况你考虑了吗

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-10 23:49 ·1 浏览 ·0 回复

写组件测试的时候,最让人头疼的往往不是"跑不跑得通",而是那些看起来跑通了、实际上什么都没验证的测试。绿油油的覆盖率背后,可能藏着一堆"删掉断言也不会挂"的用例。用 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 掉,比在每个用例里打补丁优雅得多。

结语

组件测试的价值从来不是覆盖率数字,而是它能不能在你重构时给出真实反馈。上面这些边界情况,绕来绕去都在回答同一个问题:我这条断言,到底验证了什么?如果把它删掉测试依然通过,那它大概率没在干活。写测试花的时间和写组件差不多,但省下来的调试时间,通常要多得多。

本文转载自 阿乐技术社区,原文地址:https://www.leleweb.cn/thread-218.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~