阿里云主机折上折
  • 微信号
Current Site:Index > Vitest testing framework

Vitest testing framework

Author:Chuan Chen 阅读数:18380人阅读 分类: Vue.js

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:

  1. Debugging in VS Code:
// launch.json configuration
{
  "type": "node",
  "request": "launch",
  "name": "Debug Current Test File",
  "runtimeExecutable": "npm",
  "runtimeArgs": ["test", "--", "${relativeFile}"]
}
  1. 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

  1. Resolving "document is not defined" error:
// In vite.config.js
test: {
  environment: 'jsdom'
}
  1. Handling CSS imports:
// Install necessary packages
npm install -D @vitest/css
  1. 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

上一篇:Element Plus等UI框架

下一篇:Vue3 DevTools

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.