单元测试与组件测试
单元测试(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% |
Cao Note