单元测试规范
单元测试是保障代码质量的重要手段,通过规范的单元测试实践可以有效减少缺陷、提升代码可维护性。前端领域技术栈多样,单元测试需要结合框架特性和业务场景制定具体方案。
单元测试的核心价值
单元测试针对代码最小可测试单元进行验证,通常指函数或类方法。良好的单元测试能带来三方面收益:
- 快速反馈:在代码修改后立即验证逻辑正确性,相比手动测试效率提升显著
- 设计改进:编写测试时会自然促使代码解耦,提高模块化程度
- 文档作用:测试用例本身就是代码行为的活文档
// 示例:测试纯函数
function add(a, b) {
return a + b
}
describe('add function', () => {
it('should return 5 when adding 2 and 3', () => {
expect(add(2, 3)).toBe(5)
})
it('should handle negative numbers', () => {
expect(add(-1, -1)).toBe(-2)
})
})
前端单元测试框架选型
主流前端测试框架各有侧重:
- Jest:Facebook推出的零配置方案,内置断言库和覆盖率工具
- Vitest:基于Vite的极速测试方案,兼容Jest语法
- Mocha + Chai:灵活组合方案,适合需要深度定制的场景
// Vitest示例
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
describe('Button Component', () => {
it('renders slot content', () => {
const wrapper = mount(Button, {
slots: { default: 'Click Me' }
})
expect(wrapper.text()).toContain('Click Me')
})
})
测试覆盖率标准
覆盖率指标应包含:
- 行覆盖率:至少达到80%
- 分支覆盖率:条件语句需测试所有分支
- 函数覆盖率:所有导出函数都应被测试
通过.nycrc
文件配置阈值:
{
"check-coverage": true,
"lines": 80,
"branches": 75,
"functions": 85,
"statements": 80
}
组件测试最佳实践
UI组件测试需关注:
- 渲染验证:检查DOM结构和样式类
- 交互测试:模拟用户事件触发
- Props验证:测试不同属性组合下的表现
// React组件测试示例
import { render, screen, fireEvent } from '@testing-library/react'
import Toggle from './Toggle'
test('toggles state when clicked', () => {
render(<Toggle />)
const button = screen.getByRole('switch')
expect(button).toHaveAttribute('aria-checked', 'false')
fireEvent.click(button)
expect(button).toHaveAttribute('aria-checked', 'true')
})
异步代码测试模式
处理异步逻辑的三种方式:
- 回调检测:使用
done
参数 - Promise返回:测试框架自动等待
- async/await:最直观的现代写法
// 异步测试示例
describe('fetchUser API', () => {
it('resolves with user data', async () => {
const user = await fetchUser(123)
expect(user).toHaveProperty('id', 123)
})
it('rejects when user not found', () => {
await expect(fetchUser(999)).rejects.toThrow('Not Found')
})
})
测试数据管理策略
测试数据组织方式直接影响用例可维护性:
- 工厂函数:动态生成测试数据
- 固定夹具:JSON文件存储静态数据
- 随机数据:使用faker.js生成仿真数据
// 工厂函数示例
const createProduct = (overrides = {}) => ({
id: faker.datatype.uuid(),
name: faker.commerce.productName(),
price: faker.commerce.price(),
...overrides
})
describe('购物车逻辑', () => {
it('计算总价应包含商品价格', () => {
const cart = {
items: [createProduct({ price: 100 }), createProduct({ price: 200 })]
}
expect(calculateTotal(cart)).toBe(300)
})
})
测试性能优化
大型项目测试提速方案:
- 并行执行:Jest的
--maxWorkers
参数 - 文件过滤:只运行修改文件的关联测试
- 模拟优化:使用轻量级mock替代完整实现
# 并行执行测试
jest --maxWorkers=4
# 只运行与修改文件相关的测试
jest --onlyChanged
持续集成中的测试
CI流水线关键配置点:
- 缓存策略:缓存node_modules和jest缓存目录
- 分段执行:将单元测试与E2E测试分开运行
- 失败重试:对偶发失败设置自动重试
# GitHub Actions配置示例
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v3
with:
path: |
**/node_modules
**/.jest-cache
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- run: yarn test:unit --ci --runInBand
常见反模式规避
需要避免的测试实践:
- 过度实现验证:测试内部实现而非行为
- 脆弱选择器:依赖DOM结构而非语义化属性
- 全局状态泄漏:测试之间未正确重置状态
// 反模式示例:测试实现细节
test('不应该直接测试内部状态', () => {
const instance = new Component()
expect(instance._internalState).toBe(0) // 错误做法
// 应改为测试公开行为
instance.doSomething()
expect(instance.getResult()).toBe(1)
})
测试代码维护原则
保持测试代码质量的要点:
- DRY原则:通过setup函数复用公共逻辑
- 描述清晰:测试描述应包含预期行为和上下文
- 分层组织:按功能域划分测试文件结构
// 测试组织结构示例
src/
components/
Button/
Button.tsx
Button.test.tsx # 单元测试
Button.stories.tsx # 可视化用例
utils/
math.test.ts # 纯函数测试
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn