Skip to content

Vitest 4.0 Deep Dive: Component Testing and Snapshot Testing Best Practices

Vitest has become one of the de facto standards for frontend testing. Version 4.0 brings important improvements in snapshot testing, component testing, and parallel execution, making the testing experience smoother.

Snapshot Testing Evolution

Snapshot testing has evolved from a "controversial feature" to a "practical tool"—the key is using it correctly:

typescript
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/vue'
import UserCard from './UserCard.vue'

describe('UserCard', () => {
  it('matches snapshot for normal user', () => {
    const { container } = render(UserCard, {
      props: {
        user: {
          name: 'John',
          avatar: '/avatars/john.jpg',
          role: 'developer'
        }
      }
    })
    
    expect(container).toMatchSnapshot()
  })

  it('matches snapshot for admin user', () => {
    const { container } = render(UserCard, {
      props: {
        user: {
          name: 'Jane',
          avatar: '/avatars/jane.jpg',
          role: 'admin'
        }
      }
    })
    
    expect(container).toMatchSnapshot()
  })
})

Snapshot Testing Best Practices

  1. Small snapshots first: Only snapshot key UI fragments, not entire pages
  2. Meaningful snapshot names: Use toMatchSnapshot({ customSnapshotIdentifiers }) to add context
  3. Regular review: Don't blindly accept all snapshot updates
typescript
// Good practice: small, meaningful snapshots
it('renders loading state correctly', () => {
  const { container } = render(Button, { props: { loading: true } })
  expect(container.querySelector('.loading-spinner')).toMatchSnapshot()
})

// Bad practice: large, vague snapshots
it('renders correctly', () => {
  const { container } = render(ComplexPage)
  expect(container).toMatchSnapshot()  // Snapshot too large to maintain
})

Component Testing Strategy

Test Layer Division

typescript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import LoginForm from './LoginForm.vue'

describe('LoginForm', () => {
  // Layer 1: pure rendering test
  it('renders all form fields', () => {
    const wrapper = mount(LoginForm)
    
    expect(wrapper.find('input[name="email"]').exists()).toBe(true)
    expect(wrapper.find('input[name="password"]').exists()).toBe(true)
    expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
  })

  // Layer 2: interaction test
  it('emits submit event with form data', async () => {
    const wrapper = mount(LoginForm)
    
    await wrapper.find('input[name="email"]').setValue('test@example.com')
    await wrapper.find('input[name="password"]').setValue('password123')
    await wrapper.find('form').trigger('submit')
    
    expect(wrapper.emitted('submit')).toHaveLength(1)
    expect(wrapper.emitted('submit')[0]).toEqual([{
      email: 'test@example.com',
      password: 'password123'
    }])
  })

  // Layer 3: validation test
  it('shows validation error for invalid email', async () => {
    const wrapper = mount(LoginForm)
    
    await wrapper.find('input[name="email"]').setValue('invalid-email')
    await wrapper.find('form').trigger('submit')
    
    expect(wrapper.find('.error-message').text()).toContain('Invalid email format')
  })
})

Parallel Testing Optimization

Vitest 4.0's parallel testing performance has been significantly improved:

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    // Parallel testing configuration
    threads: true,
    maxThreads: 4,
    minThreads: 2,
    
    // Test file isolation
    isolate: true,
    
    // Test timeout
    testTimeout: 10000,
    hookTimeout: 10000,
    
    // Retry mechanism
    retry: 2
  }
})

Parallel Testing Considerations

  1. Avoid shared state: Each test should be independent, not relying on other tests' state
  2. Use beforeEach for cleanup: Reset state before each test
  3. Control parallelism reasonably: Too much parallelism may cause resource contention
typescript
describe('Database Tests', () => {
  let db

  beforeEach(async () => {
    // Create new database connection before each test
    db = await createTestDatabase()
  })

  afterEach(async () => {
    // Clean up after each test
    await db.cleanup()
  })

  it('creates user', async () => {
    const user = await db.createUser({ name: 'John' })
    expect(user.id).toBeDefined()
  })

  it('finds user by id', async () => {
    const created = await db.createUser({ name: 'Jane' })
    const found = await db.findUser(created.id)
    expect(found.name).toBe('Jane')
  })
})

Test Coverage Strategy

typescript
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/types/',
        '*.config.*'
      ],
      thresholds: {
        statements: 80,
        branches: 75,
        functions: 80,
        lines: 80
      }
    }
  }
})

Summary

Vitest 4.0 makes frontend testing faster and more reliable. The key to snapshot testing is "small and precise," component testing is "clearly layered," and parallel testing is "isolated state." In real projects, reasonable testing strategy is more important than pursuing 100% coverage—testing is to verify behavior, not to satisfy numerical indicators.

MIT Licensed