我在真实项目中落地的前端测试实践与避坑指南
谁更灵活?谁更省事?Vitest、Jest 和 Cypress 的真实对比
我写这篇不是因为闲得慌,是上周又在 CI 上被 Jest 卡了 4 分钟——就跑 80 个单元测试,它自己先干起了“加载动画”,还带内存泄漏警告。我盯着终端发呆的那两分钟,突然觉得:得理一理,到底该用啥测前端。
我目前主力项目用的是 Vitest,但老项目还在跑 Jest,E2E 测 UI 则切到了 Cypress(虽然最近也在试 Playwright)。今天不搞虚的,直接上真实代码、真实耗时、真实报错截图(文字描述版)、真实踩坑点——全是我在 jztheme.com 这个主题项目里实打实调过的。
先说结论:Vitest 是我现在默认首选
不是因为它多先进,而是它真的“不折腾”。开箱即用、HMR 支持丝滑、和 Vite 生态咬合得像原生一样。Jest 稳,但太重;Cypress 强,但只适合 E2E。别听别人说“全栈统一 Jest”,我信了三年,直到某天发现 mock 一个 fetch 就要配 5 个插件、改 3 个 config 文件……我放弃了。
Vitest:快、轻、顺手,但有些地方真得自己兜底
我一般这样写一个组件测试:
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import MyButton from './MyButton.vue'
describe('MyButton', () => {
it('renders with label', () => {
const wrapper = mount(MyButton, {
props: { label: '点击我' }
})
expect(wrapper.text()).toContain('点击我')
})
it('emits click event', async () => {
const wrapper = mount(MyButton)
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
})
})
这玩意儿跑起来比 Jest 快 3 倍不止。本地 dev 模式下改完代码,测试几乎是秒出结果。而且不用额外装 @babel/preset-env 或 ts-jest,Vite 那套解析器直接复用,连 tsconfig.json 都不用动。
但注意一个坑:Vitest 默认不支持 DOM 断言里的 toBeInTheDocument() 这种 Jest 风格 API。你得手动加 @testing-library/vue,或者直接用 wrapper.find().exists()——我选后者,少装一个包,少一个潜在 bug 源。
Jest:稳如老狗,慢如老牛
老项目还在用 Jest,主要是历史包袱+团队习惯。配置长这样:
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
transform: {
'^.+\.vue$': '@vue/vue3-jest',
},
}
光 setupTests.ts 就要手动 mock localStorage、fetch、IntersectionObserver……有一次我忘了 mock ResizeObserver,测试在 CI 上全挂,本地却好好的——因为 Mac 上的 jsdom 版本跟 Ubuntu 不一致。折腾半天才发现是这个破 Observer。
还有个隐藏雷:Jest 的 jest.mock() 是 hoist 的,但如果你在 beforeEach 里 mock,它可能不生效。我踩过三次,每次都要翻 Jest 文档第 7 页才能找到那行小字提醒。
优点?稳定性是真的强。我们线上有个核心支付流程的单元测试,跑了三年没坏过。但它不适合快速迭代的小项目——启动一次测试要 1.8 秒,加上 watch 模式卡顿,开发体验就是“写一行,等三秒,再写”。
Cypress:E2E 的事实标准,但别拿它测函数
我用 Cypress 测的是用户真实路径:登录 → 选主题 → 点击「应用」→ 看是否跳转成功 → 检查 localStorage 是否存了 themeId。
describe('Theme Switch Flow', () => {
it('applies dark theme and persists', () => {
cy.visit('https://jztheme.com/demo')
cy.get('[data-testid="theme-toggle"]').click()
cy.url().should('include', '/dark')
cy.window().then((win) => {
expect(win.localStorage.getItem('theme')).to.eq('dark')
})
})
})
这段代码在我本地和 GitHub Actions 上都稳定跑通。Cypress 的录制回放、时间旅行调试、自动等待机制,真的救过我命——有次按钮 disabled 状态没及时更新,它自动等了 4 秒才点,而 Jest + fireEvent 会直接报错“元素不可点击”。
但千万别用它测工具函数。我见过有人写 cy.then(() => expect(utils.formatDate()).toBe('2024-06-12')) ——这已经不是测试,是行为艺术。Cypress 是浏览器环境,启动开销大,单测一个字符串格式化函数,比 Vitest 慢 20 倍。
我的选型逻辑:按场景分层,不硬套一套方案
- 单元测试(工具函数、组合式 API)→ Vitest:快、轻、TypeScript 友好,错误堆栈清晰到能直接定位到
.ts行号 - 组件测试(Vue/React 组件)→ Vitest + @vue/test-utils 或 @testing-library/react:不用切环境,mock 成本低,HMR 响应快
- 集成测试(跨组件交互、状态流转)→ Vitest + 自定义 render 函数:比如模拟全局 store、router,比 Jest 的
createMockInstance直观得多 - E2E/UI 流程测试 → Cypress:断言真实 DOM、网络请求拦截、跨 tab 测试,它仍是目前最稳的选择
- 性能敏感的 CI 流水线 → Vitest –run + –no-watch:我们 GitHub Actions 里,Vitest 全量跑 120 个测试平均 12 秒,Jest 同样用例要 48 秒
至于 Playwright?我试过两周,API 更底层、多浏览器支持确实香,但调试体验不如 Cypress 直观,错误信息也更“计算机式”。目前只在需要测 Safari 表现时临时切过去,日常还是 Cypress。
最后补一句实在话
没有银弹。Vitest 在某些 Web Worker 场景下 mock 还不太稳(我遇到过 self undefined),Cypress 对 Service Worker 支持仍有坑(比如离线缓存测试得手动关掉 SW)。但我宁愿接受这些小问题,也不愿回到 Jest 的 config 泥潭里反复挣扎。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流——尤其是你怎么搞定 IntersectionObserver 的 mock,我真的受够了。

暂无评论