Vitest testing framework
Vitest Testing Framework
Vitest is a next-generation testing framework based on Vite, specifically designed for Vue.js applications. It inherits Vite's fast startup and hot update features while providing a comprehensive testing solution. Compared to Jest, Vitest performs exceptionally well in Vue projects, especially in Single File Component (SFC) testing.
Why Choose Vitest
Vitest's deep integration with the Vue ecosystem is its greatest advantage. It natively supports:
- Vue Single File Component testing
- Composition API
- Vuex/Pinia state management
- Vue Router
// Example: Testing a simple Vue component
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')
})
Installation and Configuration
Installing Vitest is straightforward, especially in Vite projects:
npm install -D vitest @vue/test-utils
Add test configuration to 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'
}
})
Testing Vue Components
Vitest works perfectly with @vue/test-utils, making it easy to test various Vue component scenarios:
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)
})
})
Testing Composition API
For components using the Composition API, Vitest provides a more intuitive testing approach:
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)
})
Mocking and Stubbing
Vitest has built-in powerful mocking capabilities, making it easy to mock modules, functions, and components:
import { vi } from 'vitest'
// Mock a module
vi.mock('../api', () => ({
fetchData: vi.fn(() => Promise.resolve({ data: 'mocked data' }))
}))
// Using mocks in tests
test('uses mocked API', async () => {
const { fetchData } = await import('../api')
const result = await fetchData()
expect(result.data).toBe('mocked data')
})
Testing Asynchronous Logic
Vitest provides comprehensive support for asynchronous testing:
test('async test', async () => {
const result = await fetchData()
expect(result).toEqual({ status: 'success' })
})
// Or using callback style
test('callback test', (done) => {
fetchData((err, result) => {
expect(err).toBeNull()
expect(result).toBeDefined()
done()
})
})
Snapshot Testing
Vitest's snapshot testing is compatible with Jest but faster:
import { render } from '@testing-library/vue'
import MyComponent from './MyComponent.vue'
test('matches snapshot', () => {
const { html } = render(MyComponent)
expect(html()).toMatchSnapshot()
})
Coverage Reports
Generating test coverage reports is simple:
vitest run --coverage
Configure coverage in vite.config.js:
test: {
coverage: {
provider: 'istanbul', // or 'c8'
reporter: ['text', 'json', 'html']
}
}
CI/CD Integration
Vitest can be easily integrated into various CI/CD workflows:
# GitHub Actions example
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
Debugging Tests
Vitest offers multiple debugging options:
- Debugging in VS Code:
// launch.json configuration
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"runtimeExecutable": "npm",
"runtimeArgs": ["test", "--", "${relativeFile}"]
}
- Browser debugging:
vitest --ui
Performance Optimization
Vitest performs well in large projects but still has room for optimization:
// Use test.only or test.skip to control test scope
test.only('critical test', () => {
// Only run this test
})
// Parallel test execution
test.concurrent('parallel test', async () => {
// Runs in parallel with other .concurrent tests
})
Common Issue Resolution
- Resolving "document is not defined" error:
// In vite.config.js
test: {
environment: 'jsdom'
}
- Handling CSS imports:
// Install necessary packages
npm install -D @vitest/css
- Resolving Vue version conflicts:
// In vite.config.js
resolve: {
dedupe: ['vue']
}
Advanced Testing Patterns
For complex scenarios, Vitest provides various advanced features:
// Testing custom directives
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')
})
// Testing slot content
test('slot content', () => {
const wrapper = mount(MyComponent, {
slots: {
default: 'Main Content',
footer: '<div>Footer</div>'
}
})
expect(wrapper.text()).toContain('Main Content')
})
Test-Driven Development (TDD) Practices
Vitest is well-suited for TDD workflows:
// 1. Write the test first
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5)
})
// 2. Implement the functionality
function add(a, b) {
return a + b
}
// 3. Refactor and optimize
function add(...numbers) {
return numbers.reduce((sum, num) => sum + num, 0)
}
Testing Vue Router
Testing routing-related logic:
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)
})
Testing Pinia State Management
Testing Pinia stores:
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)
})
Testing HTTP Requests
Mocking HTTP requests with Vitest:
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' })
})
Testing Environment Variables
Handling environment variable testing:
// Set environment variables before testing
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')
})
Testing Error Boundaries
Testing component error handling:
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')
})
Testing Custom Hooks
Testing custom Composition API hooks:
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)
// Simulate window resize
window.innerWidth = 500
window.innerHeight = 300
window.dispatchEvent(new Event('resize'))
expect(width.value).toBe(500)
expect(height.value).toBe(300)
})
Testing Performance-Sensitive Code
Performance testing with Vitest:
test('performance test', () => {
const start = performance.now()
// Execute the code to be tested
heavyCalculation()
const duration = performance.now() - start
expect(duration).toBeLessThan(100) // Should complete within 100ms
})
Testing TypeScript Projects
Vitest has excellent TypeScript support:
// tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
// Test file
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')
})
})
Testing Third-Party Library Integration
Testing integration with third-party libraries:
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)
})
Testing Animations and Transitions
Testing Vue transition effects:
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)
})
Testing Form Validation
Testing form validation logic:
test('form validation', async () => {
const wrapper = mount(FormComponent)
const emailInput = wrapper.find('input[type="email"]')
// Test invalid email
await emailInput.setValue('invalid-email')
await wrapper.find('form').trigger('submit')
expect(wrapper.find('.error').text()).toContain('Invalid email')
// Test valid email
await emailInput.setValue('valid@example.com')
await wrapper.find('form').trigger('submit')
expect(wrapper.find('.error').exists()).toBe(false)
})
Testing Custom Events
Testing custom events emitted by components:
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'])
})
Testing provide/inject
Testing component communication via 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')
})
Testing Dynamic Components
Testing dynamic component switching:
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)
})
Testing SSR Compatibility
Testing server-side rendering compatibility:
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>')
})
Testing Web Components
Testing Web Components in Vue:
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')
})
Testing Accessibility
Testing component accessibility:
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)
})
Testing Internationalization
Testing multilingual support:
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')
})
Testing Responsive Design
Testing responsive layouts:
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)
})
Testing Web Workers
Testing interaction with Web Workers:
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')
})
Testing WebSocket Connections
Testing WebSocket interaction:
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')
})
Testing Browser APIs
Testing browser API calls:
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)
})
Testing File Uploads
Testing file upload functionality:
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)
})
Testing Print Functionality
Testing print-related features:
test('print functionality', () => {
global.print = vi.fn()
const wrapper = mount(PrintComponent)
wrapper.find('.print-button').trigger('click')
expect(global.print).toHaveBeenCalled()
})
Testing Clipboard Operations
Testing clipboard 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