"又出线上问题了!"周一早会上,我们的技术总监语气严肃。一个简单的代码改动,却引发了一连串的问题。作为前端负责人,我深感愧疚。这已经是本月第三次类似的事故了。
回顾这些问题,我们发现一个共同点:都是因为代码改动引发了意想不到的副作用。如果有完善的单元测试,这些问题本可以在开发阶段就被发现。于是,我们决定系统性地构建前端测试体系。
现状分析
首先我们统计了一下现有的测试情况:
- 测试覆盖率不到 20%
- 大多是集成测试,运行时间长
- 测试代码质量参差不齐
- 团队缺乏测试习惯
就像一座没有安全检查的大楼,随时可能出现问题。我们需要从基础开始,建立起完整的测试防护网。
测试策略
经过团队讨论,我们制定了分层测试策略。就像建筑的地基、框架、装修一样,每一层都有其特定的职责:
typescript
// 工具函数测试示例
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2024-12-03')
expect(formatDate(date)).toBe('2024-12-03')
expect(formatDate(date, 'YYYY/MM/DD')).toBe('2024/12/03')
})
it('should handle invalid date', () => {
expect(formatDate(null)).toBe('-')
expect(formatDate(undefined)).toBe('-')
expect(formatDate('invalid')).toBe('-')
})
})
// React 组件测试示例
describe('Button', () => {
it('should render children correctly', () => {
const { getByText } = render(<Button>Click me</Button>)
expect(getByText('Click me')).toBeInTheDocument()
})
it('should handle click events', () => {
const handleClick = jest.fn()
const { getByRole } = render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should show loading state', () => {
const { getByRole, getByTestId } = render(<Button loading>Loading</Button>)
expect(getByRole('button')).toBeDisabled()
expect(getByTestId('loading-spinner')).toBeInTheDocument()
})
})
测试工具链
我们精心挑选了一套测试工具链:
typescript
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': 'identity-obj-proxy'
},
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/*.stories.{ts,tsx}'],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
// jest.setup.ts
import '@testing-library/jest-dom'
import 'jest-canvas-mock'
import { server } from './src/mocks/server'
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
测试规范
为了保证测试的质量和一致性,我们制定了测试规范:
typescript
// 测试文件结构示例
describe('UserProfile', () => {
// 准备测试数据
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com'
}
// 分组测试用例
describe('rendering', () => {
it('should render user info correctly', () => {
const { getByText } = render(<UserProfile user={mockUser} />)
expect(getByText(mockUser.name)).toBeInTheDocument()
expect(getByText(mockUser.email)).toBeInTheDocument()
})
it('should show loading state', () => {
const { getByTestId } = render(<UserProfile loading />)
expect(getByTestId('loading-spinner')).toBeInTheDocument()
})
})
describe('interactions', () => {
it('should handle edit button click', () => {
const onEdit = jest.fn()
const { getByRole } = render(<UserProfile user={mockUser} onEdit={onEdit} />)
fireEvent.click(getByRole('button', { name: /edit/i }))
expect(onEdit).toHaveBeenCalledWith(mockUser.id)
})
})
describe('error handling', () => {
it('should show error message', () => {
const error = 'Failed to load user'
const { getByText } = render(<UserProfile error={error} />)
expect(getByText(error)).toBeInTheDocument()
})
})
})
测试自动化
我们将测试集成到了开发流程中:
typescript
// 自动化测试脚本
const runTests = async changedFiles => {
// 根据变更文件确定测试范围
const testPatterns = getTestPatterns(changedFiles)
// 运行测试
const results = await jest.runCLI({
selectProjects: testPatterns,
onlyChanged: true,
coverage: true
})
// 生成测试报告
await generateReport(results)
// 更新测试覆盖率徽章
await updateCoverageBadge(results.coverage)
}
// Git hooks 配置
module.exports = {
hooks: {
'pre-commit': 'lint-staged && npm test',
'pre-push': 'npm run test:coverage'
}
}
实践效果
经过三个月的努力,我们取得了显著的成效:
- 测试覆盖率提升到 85%
- 线上问题减少了 70%
- 代码重构更有信心
- 团队形成了测试文化
最让我印象深刻的是一位同事的反馈:"有了测试,改代码的时候终于不用提心吊胆了。"
经验总结
前端测试就像是给代码买保险,虽然前期需要投入,但能避免更大的损失。我们的经验是:
从小处着手 - 先为核心功能写测试循序渐进 - 一步步提高覆盖率重视规范 - 建立统一的测试标准持续改进 - 不断优化测试流程
写在最后
前端测试不是可有可无的装饰,而是保证代码质量的重要手段。就像那句话说的:"测试是开发者的安全网,也是用户的保障。"
有什么问题欢迎在评论区讨论,让我们一起探讨前端测试的最佳实践!
如果觉得有帮助,别忘了点赞关注,我会继续分享更多实战经验~