Vitest测试框架
Vitest测试框架
Vitest是一个基于Vite的下一代测试框架,专为Vue.js应用设计。它继承了Vite的快速启动和热更新特性,同时提供了完整的测试解决方案。与Jest相比,Vitest在Vue项目中表现更出色,特别是在单文件组件(SFC)测试方面。
为什么选择Vitest
Vitest与Vue生态系统的深度集成是其最大优势。它原生支持:
- Vue单文件组件测试
- Composition API
- Vuex/Pinia状态管理
- Vue Router
// 示例:测试一个简单的Vue组件
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
test('increments counter', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.find('span').text()).toBe('1')
})
安装与配置
安装Vitest非常简单,特别是在Vite项目中:
npm install -D vitest @vue/test-utils
在vite.config.js中添加测试配置:
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom'
}
})
测试Vue组件
Vitest与@vue/test-utils完美配合,可以轻松测试各种Vue组件场景:
import { shallowMount } from '@vue/test-utils'
import MyComponent from '../MyComponent.vue'
describe('MyComponent', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(MyComponent, {
props: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})
测试Composition API
对于使用Composition API的组件,Vitest提供了更直观的测试方式:
import { ref } from 'vue'
import { useCounter } from './useCounter'
test('useCounter', () => {
const { count, increment } = useCounter()
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
模拟与桩
Vitest内置了强大的模拟功能,可以轻松模拟模块、函数和组件:
import { vi } from 'vitest'
// 模拟模块
vi.mock('../api', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'mocked data' }))
}))
// 测试中使用模拟
test('uses mocked API', async () => {
const { fetchData } = await import('../api')
const result = await fetchData()
expect(result.data).toBe('mocked data')
})
测试异步逻辑
Vitest对异步测试提供了全面支持:
test('async test', async () => {
const result = await fetchData()
expect(result).toEqual({ status: 'success' })
})
// 或者使用回调风格
test('callback test', (done) => {
fetchData((err, result) => {
expect(err).toBeNull()
expect(result).toBeDefined()
done()
})
})
快照测试
Vitest的快照测试功能与Jest兼容但更快速:
import { render } from '@testing-library/vue'
import MyComponent from './MyComponent.vue'
test('matches snapshot', () => {
const { html } = render(MyComponent)
expect(html()).toMatchSnapshot()
})
覆盖率报告
生成测试覆盖率报告非常简单:
vitest run --coverage
在vite.config.js中配置覆盖率:
test: {
coverage: {
provider: 'istanbul', // 或 'c8'
reporter: ['text', 'json', 'html']
}
}
与CI/CD集成
Vitest可以轻松集成到各种CI/CD流程中:
# GitHub Actions示例
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm ci
- run: npm test
调试测试
Vitest提供了多种调试选项:
- 在VS Code中调试:
// launch.json配置
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"runtimeExecutable": "npm",
"runtimeArgs": ["test", "--", "${relativeFile}"]
}
- 使用浏览器调试:
vitest --ui
性能优化
Vitest在大型项目中表现出色,但仍有优化空间:
// 使用test.only或test.skip控制测试范围
test.only('critical test', () => {
// 只运行这个测试
})
// 并行执行测试
test.concurrent('parallel test', async () => {
// 与其他.concurrent测试并行运行
})
常见问题解决
- 解决"document is not defined"错误:
// 在vite.config.js中
test: {
environment: 'jsdom'
}
- 处理CSS导入:
// 安装必要的包
npm install -D @vitest/css
- 解决Vue版本冲突:
// 在vite.config.js中
resolve: {
dedupe: ['vue']
}
高级测试模式
对于复杂场景,Vitest提供了多种高级功能:
// 测试自定义指令
test('custom directive', () => {
const wrapper = mount(MyComponent, {
global: {
directives: {
highlight: {
mounted(el) {
el.style.color = 'red'
}
}
}
}
})
expect(wrapper.find('p').element.style.color).toBe('red')
})
// 测试插槽内容
test('slot content', () => {
const wrapper = mount(MyComponent, {
slots: {
default: 'Main Content',
footer: '<div>Footer</div>'
}
})
expect(wrapper.text()).toContain('Main Content')
})
测试驱动开发(TDD)实践
Vitest非常适合TDD工作流:
// 1. 先写测试
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5)
})
// 2. 实现功能
function add(a, b) {
return a + b
}
// 3. 重构优化
function add(...numbers) {
return numbers.reduce((sum, num) => sum + num, 0)
}
测试Vue Router
测试路由相关逻辑:
import { createRouter, createWebHistory } from 'vue-router'
import { mount } from '@vue/test-utils'
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/', component: Home }]
})
test('navigates to home', async () => {
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
await router.push('/')
expect(wrapper.findComponent(Home).exists()).toBe(true)
})
测试Pinia状态管理
测试Pinia store:
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from './stores/counter'
beforeEach(() => {
setActivePinia(createPinia())
})
test('increments counter', () => {
const counter = useCounterStore()
expect(counter.count).toBe(0)
counter.increment()
expect(counter.count).toBe(1)
})
测试HTTP请求
使用Vitest模拟HTTP请求:
import { vi } from 'vitest'
import axios from 'axios'
import { fetchUser } from './api'
vi.mock('axios')
test('fetches user', async () => {
axios.get.mockResolvedValue({ data: { id: 1, name: 'John' } })
const user = await fetchUser(1)
expect(user).toEqual({ id: 1, name: 'John' })
})
测试环境变量
处理环境变量的测试:
// 测试前设置环境变量
import { vi } from 'vitest'
beforeEach(() => {
vi.stubEnv('API_URL', 'https://test.api')
})
test('uses test API', () => {
expect(import.meta.env.API_URL).toBe('https://test.api')
})
测试错误边界
测试组件错误处理:
test('handles errors', () => {
const ErrorComponent = {
setup() {
throw new Error('Test error')
},
render() {}
}
const wrapper = mount(ErrorBoundary, {
slots: { default: ErrorComponent }
})
expect(wrapper.text()).toContain('Something went wrong')
})
测试自定义hook
测试自定义Composition API hook:
import { useWindowSize } from './useWindowSize'
import { vi } from 'vitest'
test('tracks window size', () => {
const { width, height } = useWindowSize()
expect(width.value).toBe(window.innerWidth)
expect(height.value).toBe(window.innerHeight)
// 模拟窗口大小变化
window.innerWidth = 500
window.innerHeight = 300
window.dispatchEvent(new Event('resize'))
expect(width.value).toBe(500)
expect(height.value).toBe(300)
})
测试性能敏感代码
使用Vitest测试性能:
test('performance test', () => {
const start = performance.now()
// 执行需要测试的代码
heavyCalculation()
const duration = performance.now() - start
expect(duration).toBeLessThan(100) // 应在100ms内完成
})
测试TypeScript项目
Vitest对TypeScript有很好的支持:
// tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
// 测试文件
import { describe, it, expect } from 'vitest'
interface User {
id: number
name: string
}
describe('type tests', () => {
it('assigns correct types', () => {
const user: User = {
id: 1,
name: 'John'
}
expect(user.name).toBe('John')
})
})
测试第三方库集成
测试与第三方库的集成:
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { useCartStore } from '@/stores/cart'
import CartComponent from '@/components/Cart.vue'
test('cart integration', () => {
const wrapper = mount(CartComponent, {
global: {
plugins: [createTestingPinia()]
}
})
const cart = useCartStore()
cart.addItem({ id: 1, name: 'Product' })
expect(wrapper.findAll('.cart-item')).toHaveLength(1)
})
测试动画和过渡
测试Vue过渡效果:
test('fade transition', async () => {
const wrapper = mount(TransitionComponent)
expect(wrapper.find('.fade').exists()).toBe(false)
await wrapper.setProps({ show: true })
expect(wrapper.find('.fade').isVisible()).toBe(true)
await wrapper.setProps({ show: false })
expect(wrapper.find('.fade').exists()).toBe(false)
})
测试表单验证
测试表单验证逻辑:
test('form validation', async () => {
const wrapper = mount(FormComponent)
const emailInput = wrapper.find('input[type="email"]')
// 测试无效邮箱
await emailInput.setValue('invalid-email')
await wrapper.find('form').trigger('submit')
expect(wrapper.find('.error').text()).toContain('Invalid email')
// 测试有效邮箱
await emailInput.setValue('valid@example.com')
await wrapper.find('form').trigger('submit')
expect(wrapper.find('.error').exists()).toBe(false)
})
测试自定义事件
测试组件发出的自定义事件:
test('emits custom event', async () => {
const wrapper = mount(EventComponent)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('custom-event')).toBeTruthy()
expect(wrapper.emitted('custom-event')[0]).toEqual(['payload'])
})
测试provide/inject
测试组件间的provide/inject:
test('provides data to child', () => {
const wrapper = mount(ParentComponent, {
global: {
provide: {
theme: 'dark'
}
}
})
const child = wrapper.findComponent(ChildComponent)
expect(child.vm.theme).toBe('dark')
})
测试动态组件
测试动态组件切换:
test('switches dynamic components', async () => {
const wrapper = mount(DynamicComponentContainer)
expect(wrapper.findComponent(ComponentA).exists()).toBe(true)
await wrapper.setProps({ current: 'component-b' })
expect(wrapper.findComponent(ComponentB).exists()).toBe(true)
})
测试SSR兼容性
测试服务端渲染兼容性:
import { renderToString } from '@vue/server-renderer'
import { createSSRApp } from 'vue'
import App from './App.vue'
test('SSR rendering', async () => {
const app = createSSRApp(App)
const html = await renderToString(app)
expect(html).toContain('<div>Server rendered</div>')
})
测试Web组件
测试Vue中的Web组件:
test('web component integration', async () => {
customElements.define('my-element', class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<span>Web Component</span>'
}
})
const wrapper = mount(WebComponentWrapper)
await wrapper.find('my-element').trigger('connected')
expect(wrapper.find('my-element span').text()).toBe('Web Component')
})
测试可访问性
测试组件的可访问性:
import { axe } from 'vitest-axe'
import { mount } from '@vue/test-utils'
test('accessibility check', async () => {
const wrapper = mount(AccessibleComponent)
const results = await axe(wrapper.element)
expect(results.violations).toHaveLength(0)
})
测试国际化
测试多语言支持:
test('displays correct language', async () => {
const wrapper = mount(I18nComponent, {
global: {
plugins: [createI18n({
locale: 'fr',
messages: {
fr: { greeting: 'Bonjour' },
en: { greeting: 'Hello' }
}
})]
}
})
expect(wrapper.text()).toContain('Bonjour')
await wrapper.vm.$i18n.locale = 'en'
expect(wrapper.text()).toContain('Hello')
})
测试响应式设计
测试响应式布局:
test('responsive layout', async () => {
window.innerWidth = 500
window.dispatchEvent(new Event('resize'))
const wrapper = mount(ResponsiveComponent)
expect(wrapper.find('.mobile-view').exists()).toBe(true)
window.innerWidth = 1024
window.dispatchEvent(new Event('resize'))
await wrapper.vm.$nextTick()
expect(wrapper.find('.desktop-view').exists()).toBe(true)
})
测试Web Workers
测试与Web Worker的交互:
test('web worker communication', async () => {
const worker = new Worker('./worker.js')
const result = await new Promise(resolve => {
worker.onmessage = e => resolve(e.data)
worker.postMessage('ping')
})
expect(result).toBe('pong')
})
测试WebSocket连接
测试WebSocket交互:
test('websocket connection', async () => {
const mockSocket = {
onmessage: null,
send: vi.fn(),
close: vi.fn()
}
global.WebSocket = vi.fn(() => mockSocket)
const wrapper = mount(WebSocketComponent)
mockSocket.onmessage({ data: JSON.stringify({ type: 'message', text: 'Hello' }) })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Hello')
})
测试浏览器API
测试浏览器API调用:
test('geolocation API', async () => {
const mockGeolocation = {
getCurrentPosition: vi.fn((success) =>
success({ coords: { latitude: 51.1, longitude: 45.3 } })
)
}
global.navigator.geolocation = mockGeolocation
const { latitude, longitude } = await getCurrentLocation()
expect(latitude).toBe(51.1)
expect(longitude).toBe(45.3)
})
测试文件上传
测试文件上传功能:
test('file upload', async () => {
const file = new File(['content'], 'test.txt', { type: 'text/plain' })
const wrapper = mount(UploadComponent)
const input = wrapper.find('input[type="file"]')
await input.trigger('change', {
target: { files: [file] }
})
expect(wrapper.emitted('upload')[0][0]).toEqual(file)
})
测试打印功能
测试打印相关功能:
test('print functionality', () => {
global.print = vi.fn()
const wrapper = mount(PrintComponent)
wrapper.find('.print-button').trigger('click')
expect(global.print).toHaveBeenCalled()
})
测试剪贴板操作
测试剪贴板API:
test('copy to clipboard', async () => {
global.navigator.clipboard = {
writeText: vi.fn()
}
const wrapper = mount(CopyComponent)
await wrapper.find('.copy-button').trigger('click')
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:Vue3 DevTools