阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > Vitest测试框架

Vitest测试框架

作者:陈川 阅读数:18381人阅读 分类: Vue.js

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提供了多种调试选项:

  1. 在VS Code中调试:
// launch.json配置
{
  "type": "node",
  "request": "launch",
  "name": "Debug Current Test File",
  "runtimeExecutable": "npm",
  "runtimeArgs": ["test", "--", "${relativeFile}"]
}
  1. 使用浏览器调试:
vitest --ui

性能优化

Vitest在大型项目中表现出色,但仍有优化空间:

// 使用test.only或test.skip控制测试范围
test.only('critical test', () => {
  // 只运行这个测试
})

// 并行执行测试
test.concurrent('parallel test', async () => {
  // 与其他.concurrent测试并行运行
})

常见问题解决

  1. 解决"document is not defined"错误:
// 在vite.config.js中
test: {
  environment: 'jsdom'
}
  1. 处理CSS导入:
// 安装必要的包
npm install -D @vitest/css
  1. 解决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

上一篇:Element Plus等UI框架

下一篇:Vue3 DevTools

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌