Skip to content

单元测试与组件测试 ​

单元测试(Vitest) ​

函数测试 ​

js
// utils/math.js
export const add = (a, b) => a + b
export const isEven = (n) => n % 2 === 0

// __tests__/math.test.js
import { describe, it, expect } from 'vitest'
import { add, isEven } from '../utils/math'

describe('add', () => {
  it('adds positive numbers', () => {
    expect(add(1, 2)).toBe(3)
  })

  it('adds negative numbers', () => {
    expect(add(-1, -2)).toBe(-3)
  })

  it('handles decimal numbers', () => {
    expect(add(0.1, 0.2)).toBeCloseTo(0.3)  // 浮点数用 toBeCloseTo
  })
})

describe('isEven', () => {
  it('returns true for even numbers', () => {
    expect(isEven(2)).toBe(true)
    expect(isEven(0)).toBe(true)
  })

  it('returns false for odd numbers', () => {
    expect(isEven(1)).toBe(false)
    expect(isEven(-1)).toBe(false)
  })
})
// utils/math.js
export const add = (a, b) => a + b
export const isEven = (n) => n % 2 === 0

// __tests__/math.test.js
import { describe, it, expect } from 'vitest'
import { add, isEven } from '../utils/math'

describe('add', () => {
  it('adds positive numbers', () => {
    expect(add(1, 2)).toBe(3)
  })

  it('adds negative numbers', () => {
    expect(add(-1, -2)).toBe(-3)
  })

  it('handles decimal numbers', () => {
    expect(add(0.1, 0.2)).toBeCloseTo(0.3)  // 浮点数用 toBeCloseTo
  })
})

describe('isEven', () => {
  it('returns true for even numbers', () => {
    expect(isEven(2)).toBe(true)
    expect(isEven(0)).toBe(true)
  })

  it('returns false for odd numbers', () => {
    expect(isEven(1)).toBe(false)
    expect(isEven(-1)).toBe(false)
  })
})

Promise 测试 ​

js
import { describe, it, expect } from 'vitest'
import { fetchUser } from './api'

describe('fetchUser', () => {
  it('returns user data', async () => {
    const user = await fetchUser(1)
    expect(user).toEqual({ id: 1, name: 'Alice' })
  })

  it('rejects for invalid id', async () => {
    await expect(fetchUser(-1)).rejects.toThrow('Invalid id')
  })
})
import { describe, it, expect } from 'vitest'
import { fetchUser } from './api'

describe('fetchUser', () => {
  it('returns user data', async () => {
    const user = await fetchUser(1)
    expect(user).toEqual({ id: 1, name: 'Alice' })
  })

  it('rejects for invalid id', async () => {
    await expect(fetchUser(-1)).rejects.toThrow('Invalid id')
  })
})

Vue 组件测试 ​

基础渲染测试 ​

js
// HelloWorld.vue
import { mount } from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'

describe('HelloWorld', () => {
  it('renders message', () => {
    const wrapper = mount(HelloWorld, {
      props: { msg: 'Hello Vue' },
    })
    expect(wrapper.text()).toContain('Hello Vue')
  })

  it('emits event on click', async () => {
    const wrapper = mount(HelloWorld)
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('increment')).toBeTruthy()
    expect(wrapper.emitted('increment')[0]).toEqual([1])
  })
})
// HelloWorld.vue
import { mount } from '@vue/test-utils'
import HelloWorld from './HelloWorld.vue'

describe('HelloWorld', () => {
  it('renders message', () => {
    const wrapper = mount(HelloWorld, {
      props: { msg: 'Hello Vue' },
    })
    expect(wrapper.text()).toContain('Hello Vue')
  })

  it('emits event on click', async () => {
    const wrapper = mount(HelloWorld)
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('increment')).toBeTruthy()
    expect(wrapper.emitted('increment')[0]).toEqual([1])
  })
})

组合式函数测试 ​

js
// useCounter.js
import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

// __tests__/useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from '../composables/useCounter'

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('increments the counter', () => {
    const { count, increment } = useCounter(5)
    increment()
    expect(count.value).toBe(6)
  })

  it('decrements the counter', () => {
    const { count, decrement } = useCounter(5)
    decrement()
    expect(count.value).toBe(4)
  })
})
// useCounter.js
import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

// __tests__/useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from '../composables/useCounter'

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('increments the counter', () => {
    const { count, increment } = useCounter(5)
    increment()
    expect(count.value).toBe(6)
  })

  it('decrements the counter', () => {
    const { count, decrement } = useCounter(5)
    decrement()
    expect(count.value).toBe(4)
  })
})

React 组件测试 ​

js
// Counter.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from './Counter'

describe('Counter', () => {
  it('renders with initial count', () => {
    render(<Counter />)
    expect(screen.getByText('Count: 0')).toBeInTheDocument()
  })

  it('increments on click', async () => {
    render(<Counter />)
    await userEvent.click(screen.getByRole('button', { name: /increment/i }))
    expect(screen.getByText('Count: 1')).toBeInTheDocument()
  })

  it('calls onChange handler', async () => {
    const handleChange = vi.fn()
    render(<Counter onChange={handleChange} />)
    await userEvent.click(screen.getByRole('button'))
    expect(handleChange).toHaveBeenCalledWith(1)
  })
})
// Counter.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from './Counter'

describe('Counter', () => {
  it('renders with initial count', () => {
    render(<Counter />)
    expect(screen.getByText('Count: 0')).toBeInTheDocument()
  })

  it('increments on click', async () => {
    render(<Counter />)
    await userEvent.click(screen.getByRole('button', { name: /increment/i }))
    expect(screen.getByText('Count: 1')).toBeInTheDocument()
  })

  it('calls onChange handler', async () => {
    const handleChange = vi.fn()
    render(<Counter onChange={handleChange} />)
    await userEvent.click(screen.getByRole('button'))
    expect(handleChange).toHaveBeenCalledWith(1)
  })
})

配置 Vitest ​

js
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    coverage: {
      provider: 'istanbul',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'tests/setup.ts'],
    },
  },
})
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    coverage: {
      provider: 'istanbul',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'tests/setup.ts'],
    },
  },
})

覆盖率指标 ​

指标含义目标
行覆盖率被测试执行的代码行≥ 80%
分支覆盖率if/switch 分支覆盖≥ 75%
函数覆盖率被调用的函数≥ 85%
语句覆盖率被执行的语句≥ 80%