简历怎么写
建立前端代码质量保障体系,从代码规范、提交规范、自动化测试三个维度确保代码质量。引入 ESLint、Prettier、Husky 等工具,测试覆盖率从 20% 提升至 85%,线上 Bug 数量下降 60%,代码 Review 效率提升 40%。
- 制定团队代码规范,集成 ESLint + Prettier,通过 Git Hooks 强制校验,不符合规范的代码无法提交
- 搭建自动化测试体系,使用 Vitest 编写单元测试,Playwright 编写 E2E 测试,覆盖率要求 80%+
- 实施 Commit 规范化,使用 Commitlint 校验提交信息,自动生成 Changelog,提升协作效率
- 配置 CI 质量门禁,测试不通过或覆盖率不达标的代码无法合并,保障主分支代码质量
面试怎么说
面试官: 你们是怎么保证代码质量的?
我的回答:
我们从三个层面来保障代码质量:写代码前有规范,提交代码时有校验,合并代码前有测试。
第一层是代码规范。刚开始团队没有统一规范,每个人写代码风格不一样,代码 Review 时经常为格式问题争论。我就引入了 ESLint 和 Prettier,配置了一套团队规范。ESLint 负责检查代码问题,比如未使用的变量、可能的 Bug;Prettier 负责统一格式,比如缩进、引号、分号。
然后用 Husky 配置了 Git Hooks,在代码提交前自动运行 lint,不符合规范的代码根本提交不了。这样就避免了把格式问题带到代码 Review,大家可以专注讨论业务逻辑。
第二层是提交规范。之前 commit 信息很随意,有的写"fix bug"、有的写"修改",根本不知道改了什么。我就引入了 Commitlint,强制要求 commit 必须遵循规范格式:type(scope): subject,比如 feat(user): 添加用户列表页。
这样做的好处是可以自动生成 Changelog,发版时不用手动整理改动内容,而且通过 commit 类型可以快速了解每次提交的目的。
第三层是自动化测试。之前测试覆盖率只有 20%,很多改动都是靠手工测试,效率低还容易漏测。我就推动团队写测试,单元测试用 Vitest,E2E 测试用 Playwright。
单元测试主要覆盖工具函数、业务逻辑、组件,E2E 测试覆盖核心业务流程。我们定了一个规则:新功能必须写测试,覆盖率不能低于 80%,不然代码合并不了。
现在覆盖率稳定在 85% 以上,线上 Bug 数量下降了 60%,而且重构代码时也有底气,测试跑过就没问题。
面试官: 测试覆盖率怎么保证的?
我们在 CI 里配置了质量门禁。每次提交 PR,自动跑测试,生成覆盖率报告。如果覆盖率低于 80%,PR 状态就是失败,无法合并。
另外还会展示新增代码的覆盖率,要求必须达到 90%。旧代码可以慢慢补测试,但新代码必须有测试。
在本地开发时,可以运行 npm run test:coverage 查看详细报告,知道哪些代码没覆盖到,针对性补充测试。
完整技术实现
一、ESLint + Prettier 配置
安装依赖
bash
npm install -D eslint prettier
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
npm install -D eslint-plugin-react eslint-plugin-react-hooks
npm install -D eslint-config-prettier eslint-plugin-prettier
.eslintrc.js
javascript
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
settings: {
react: {
version: 'detect'
}
},
rules: {
// 基础规则
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-debugger': 'warn',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
// TypeScript 规则
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'warn',
// React 规则
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 代码风格(由 Prettier 处理)
'prettier/prettier': 'error'
}
};
.prettierrc.js
javascript
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'es5',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'always',
endOfLine: 'lf'
};
.eslintignore / .prettierignore
node_modules
dist
build
coverage
*.min.js
public
.next
.nuxt
package.json 脚本
json
{
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
}
}
二、Husky + Commitlint
安装依赖
bash
npm install -D husky lint-staged @commitlint/cli @commitlint/config-conventional
初始化 Husky
bash
npx husky install
npm pkg set scripts.prepare="husky install"
.husky/pre-commit
bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
.husky/commit-msg
bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no -- commitlint --edit $1
.lintstagedrc.js
javascript
module.exports = {
'*.{js,jsx,ts,tsx}': [
'eslint --fix',
'prettier --write'
],
'*.{json,css,scss,md}': [
'prettier --write'
],
'*.{ts,tsx}': [
() => 'tsc --noEmit' // 类型检查
]
};
commitlint.config.js
javascript
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复 Bug
'docs', // 文档变更
'style', // 代码格式
'refactor', // 重构
'perf', // 性能优化
'test', // 测试
'build', // 构建系统
'ci', // CI 配置
'chore', // 其他修改
'revert' // 回退
]
],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'scope-case': [2, 'always', 'lower-case'],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'header-max-length': [2, 'always', 72]
}
};
正确的 Commit 示例
bash
# ✅ 正确
git commit -m "feat(user): 添加用户列表页"
git commit -m "fix(login): 修复登录失败的问题"
git commit -m "docs(readme): 更新安装文档"
# ❌ 错误
git commit -m "修改代码"
git commit -m "fix bug"
git commit -m "FEAT: add feature" # type 必须小写
三、单元测试(Vitest)
安装依赖
bash
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom
npm install -D @testing-library/user-event jsdom
vitest.config.ts
typescript
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData/**',
'src/main.tsx'
],
all: true,
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
});
src/test/setup.ts
typescript
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
afterEach(() => {
cleanup();
});
工具函数测试示例
typescript
// src/utils/format.ts
export function formatNumber(num: number): string {
return num.toLocaleString('zh-CN');
}
export function formatDate(date: Date): string {
return date.toLocaleDateString('zh-CN');
}
// src/utils/format.test.ts
import { describe, it, expect } from 'vitest';
import { formatNumber, formatDate } from './format';
describe('formatNumber', () => {
it('应该正确格式化数字', () => {
expect(formatNumber(1000)).toBe('1,000');
expect(formatNumber(1000000)).toBe('1,000,000');
});
it('应该处理小数', () => {
expect(formatNumber(1234.56)).toBe('1,234.56');
});
it('应该处理负数', () => {
expect(formatNumber(-1000)).toBe('-1,000');
});
});
describe('formatDate', () => {
it('应该正确格式化日期', () => {
const date = new Date('2024-01-01');
expect(formatDate(date)).toBe('2024/1/1');
});
});
组件测试示例
typescript
// src/components/Button/Button.tsx
import React from 'react';
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
type?: 'primary' | 'default';
}
export const Button: React.FC<ButtonProps> = ({
children,
onClick,
disabled = false,
type = 'default'
}) => {
return (
<button
className={`btn btn-${type}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
// src/components/Button/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('应该正确渲染', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('应该响应点击事件', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('禁用状态下不应该响应点击', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick} disabled>Click me</Button>);
const button = screen.getByText('Click me');
expect(button).toBeDisabled();
fireEvent.click(button);
expect(handleClick).not.toHaveBeenCalled();
});
it('应该应用正确的类名', () => {
const { rerender } = render(<Button type="primary">Primary</Button>);
expect(screen.getByText('Primary')).toHaveClass('btn-primary');
rerender(<Button type="default">Default</Button>);
expect(screen.getByText('Default')).toHaveClass('btn-default');
});
});
Hooks 测试示例
typescript
// src/hooks/useCounter.ts
import { useState, useCallback } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
const decrement = useCallback(() => {
setCount((c) => c - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
// src/hooks/useCounter.test.ts
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('应该初始化为默认值', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('应该初始化为指定值', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('应该正确递增', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('应该正确递减', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
it('应该正确重置', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
package.json 脚本
json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:ci": "vitest run --coverage --reporter=verbose"
}
}
四、E2E 测试(Playwright)
安装依赖
bash
npm install -D @playwright/test
npx playwright install
playwright.config.ts
typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000
}
});
登录流程测试
typescript
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('登录功能', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('应该显示登录表单', async ({ page }) => {
await expect(page.locator('h1')).toContainText('登录');
await expect(page.locator('input[name="username"]')).toBeVisible();
await expect(page.locator('input[name="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toBeVisible();
});
test('应该验证必填字段', async ({ page }) => {
await page.click('button[type="submit"]');
await expect(page.locator('.error-message')).toContainText('请输入用户名');
});
test('应该成功登录', async ({ page }) => {
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome-message')).toContainText('欢迎回来');
});
test('应该处理登录失败', async ({ page }) => {
await page.fill('input[name="username"]', 'wronguser');
await page.fill('input[name="password"]', 'wrongpass');
await page.click('button[type="submit"]');
await expect(page.locator('.error-toast')).toContainText('用户名或密码错误');
});
});
用户列表测试
typescript
// e2e/user-list.spec.ts
import { test, expect } from '@playwright/test';
test.describe('用户列表', () => {
test.beforeEach(async ({ page }) => {
// 登录
await page.goto('/login');
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', 'admin123');
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
// 进入用户列表
await page.click('text=用户管理');
await page.waitForURL('/users');
});
test('应该显示用户列表', async ({ page }) => {
await expect(page.locator('table')).toBeVisible();
await expect(page.locator('tbody tr')).toHaveCount(10);
});
test('应该支持搜索', async ({ page }) => {
await page.fill('input[placeholder="搜索用户"]', '张三');
await page.press('input[placeholder="搜索用户"]', 'Enter');
await page.waitForTimeout(500);
await expect(page.locator('tbody tr')).toHaveCount(1);
await expect(page.locator('tbody')).toContainText('张三');
});
test('应该支持分页', async ({ page }) => {
await expect(page.locator('.pagination')).toBeVisible();
await page.click('text=下一页');
await page.waitForTimeout(500);
await expect(page.locator('.pagination .active')).toContainText('2');
});
test('应该支持新增用户', async ({ page }) => {
await page.click('text=新增用户');
await expect(page.locator('.modal')).toBeVisible();
await page.fill('input[name="username"]', 'newuser');
await page.fill('input[name="email"]', 'newuser@example.com');
await page.fill('input[name="phone"]', '13800138000');
await page.click('.modal button[type="submit"]');
await expect(page.locator('.success-toast')).toContainText('添加成功');
});
test('应该支持编辑用户', async ({ page }) => {
await page.click('tbody tr:first-child .btn-edit');
await expect(page.locator('.modal')).toBeVisible();
await page.fill('input[name="phone"]', '13900139000');
await page.click('.modal button[type="submit"]');
await expect(page.locator('.success-toast')).toContainText('修改成功');
});
test('应该支持删除用户', async ({ page }) => {
page.on('dialog', (dialog) => dialog.accept());
await page.click('tbody tr:first-child .btn-delete');
await expect(page.locator('.success-toast')).toContainText('删除成功');
});
});
性能测试
typescript
// e2e/performance.spec.ts
import { test, expect } from '@playwright/test';
test('首页加载性能', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
console.log(`页面加载时间: ${loadTime}ms`);
expect(loadTime).toBeLessThan(3000);
// 检查性能指标
const performanceMetrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0
};
});
console.log('性能指标:', performanceMetrics);
expect(performanceMetrics.domContentLoaded).toBeLessThan(1000);
expect(performanceMetrics.firstPaint).toBeLessThan(1500);
});
package.json 脚本
json
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report"
}
}
五、CI 集成
.github/workflows/quality.yml
yaml
name: Code Quality
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 代码检查
run: npm run lint
- name: 格式检查
run: npm run format:check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 运行测试
run: npm run test:ci
- name: 上传覆盖率报告
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
- name: 检查覆盖率
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "覆盖率: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "❌ 覆盖率不足 80%"
exit 1
fi
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 安装 Playwright
run: npx playwright install --with-deps
- name: 运行 E2E 测试
run: npm run test:e2e
- name: 上传测试报告
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 30
实际问题和解决方案
问题 1:ESLint 规则太严格,团队抵触
现象: 刚引入 ESLint,团队说规则太多,提交代码很痛苦
解决方案: 渐进式推进
- 第一阶段:只开启 error 级别规则,warn 先关闭
- 第二阶段:每周增加 2-3 条规则,团队适应
- 第三阶段:配置
.eslintrc.js的overrides,对旧代码降低要求
javascript
module.exports = {
rules: {
'no-console': 'warn'
},
overrides: [
{
files: ['src/legacy/**/*.js'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}
]
};
问题 2:Prettier 和 ESLint 冲突
现象: 保存文件后,Prettier 格式化了,但 ESLint 又报错
原因: ESLint 的格式规则和 Prettier 冲突
解决方案:
bash
npm install -D eslint-config-prettier eslint-plugin-prettier
javascript
// .eslintrc.js
module.exports = {
extends: [
'eslint:recommended',
'prettier' // 必须放在最后,关闭 ESLint 的格式规则
],
plugins: ['prettier'],
rules: {
'prettier/prettier': 'error'
}
};
问题 3:Husky 在 Windows 上不工作
现象: Windows 用户提交代码时,Husky 没有执行
原因: Windows 上脚本权限问题
解决方案:
json
// package.json
{
"scripts": {
"prepare": "node -e \"try { require('husky').install() } catch (e) {}\""
}
}
或者用 simple-git-hooks 替代 Husky:
bash
npm install -D simple-git-hooks lint-staged
json
// package.json
{
"simple-git-hooks": {
"pre-commit": "npx lint-staged",
"commit-msg": "npx commitlint --edit $1"
}
}
问题 4:测试覆盖率刷不上去
现象: 写了很多测试,但覆盖率只有 60%
排查步骤:
- 运行
npm run test:coverage,查看详细报告 - 打开
coverage/index.html,红色部分是未覆盖的
常见原因:
- 工具函数的边界情况没测
- 错误处理逻辑没测
- 组件的异常状态没测
解决方案: 针对性补充测试
typescript
// 之前只测了正常情况
it('应该返回格式化后的日期', () => {
expect(formatDate(new Date('2024-01-01'))).toBe('2024/1/1');
});
// 补充边界情况
it('应该处理无效日期', () => {
expect(formatDate(new Date('invalid'))).toBe('Invalid Date');
});
it('应该处理 null', () => {
expect(() => formatDate(null as any)).toThrow();
});
问题 5:E2E 测试在 CI 上不稳定
现象: 本地跑通,CI 上时而成功时而失败
常见原因:
- 等待时间不够
- 网络请求慢
- 元素加载慢
解决方案:
typescript
// ❌ 硬编码等待
await page.waitForTimeout(3000);
// ✅ 等待元素出现
await page.waitForSelector('.user-list', { timeout: 10000 });
// ✅ 等待网络空闲
await page.waitForLoadState('networkidle');
// ✅ 等待特定请求完成
await page.waitForResponse(resp =>
resp.url().includes('/api/users') && resp.status() === 200
);
团队推广经验
第一阶段:建立规范(第 1-2 周)
目标: 制定规范,但不强制
做法:
- 配置 ESLint、Prettier,但只在 IDE 里提示
- 开会讲解每条规则的意义
- 鼓励大家在新代码里遵守
效果: 团队理解规范的价值,开始主动遵守
第二阶段:软性约束(第 3-4 周)
目标: Git Hooks 提示,但可以跳过
做法:
bash
# 允许跳过检查
git commit --no-verify -m "临时提交"
效果: 大部分人主动检查,少数人紧急时可以跳过
第三阶段:强制执行(第 5 周开始)
目标: 不符合规范的代码无法提交
做法:
- Husky 强制执行,去掉
--no-verify提示 - CI 检查覆盖率,不达标 PR 无法合并
- 代码 Review 时关注测试质量
效果: 代码质量显著提升,Bug 率下降
推广技巧
1. 数据说话
- 展示规范前后的 Bug 数量对比
- 展示测试带来的信心(重构不怕改出问题)
2. 提供工具
- 统一 IDE 配置,保存自动格式化
- 提供测试模板,降低编写门槛
3. 正向激励
- 每周表扬测试写得好的同学
- 设置"代码质量奖"
4. 以身作则
- Leader 带头写测试
- Code Review 时认真看测试
核心指标
| 指标 | 实施前 | 实施后 |
|---|---|---|
| 测试覆盖率 | 20% | 85% |
| 线上 Bug 数 | 15个/月 | 6个/月 |
| 代码 Review 时间 | 45min/次 | 25min/次 |
| 新人上手时间 | 2周 | 3天 |
| 代码格式统一 | 60% | 100% |
亮点总结
难点 1:如何让团队接受规范
挑战: 团队习惯了自由,突然加规范会抵触
解决:
- 渐进式推进,而不是一刀切
- 讲清楚规范的价值,而不是强制执行
- 提供便利工具,降低遵守成本
难点 2:如何提升测试覆盖率
挑战: 大家觉得写测试浪费时间
解决:
- 从核心业务逻辑开始,先写最重要的测试
- 提供测试模板和工具函数,降低门槛
- CI 质量门禁,不达标不能合并
难点 3:如何让测试可维护
挑战: 需求一改,测试就得全改
解决:
- 测试关注行为,而不是实现细节
- 提取测试工具函数,减少重复代码
- 使用 Page Object 模式(E2E)
typescript
// ❌ 测试实现细节
it('应该调用 setCount', () => {
const setCount = vi.fn();
render(<Counter setCount={setCount} />);
fireEvent.click(screen.getByText('+'));
expect(setCount).toHaveBeenCalledWith(1);
});
// ✅ 测试行为
it('点击加号应该增加计数', () => {
render(<Counter />);
expect(screen.getByText('0')).toBeInTheDocument();
fireEvent.click(screen.getByText('+'));
expect(screen.getByText('1')).toBeInTheDocument();
});
亮点:完整的质量保障体系
不是单纯加工具,而是建立了规范 → 校验 → 测试 → 监控的完整闭环:
- 写代码时:ESLint 提示问题
- 提交时:Husky 强制检查
- 合并时:CI 跑测试 + 覆盖率
- 上线后:监控线上错误
这样形成完整的质量保障,而不是某个环节漏掉。
总结
代码质量保障的核心是自动化 + 标准化 + 可度量。
自动化:能自动检查的,不要靠人工。ESLint、Husky、CI 都是自动化工具。
标准化:团队有统一的规范,不会因为个人习惯导致代码风格不一致。
可度量:有明确的指标(覆盖率、Bug 率),可以量化改进效果。
最重要的是渐进式推进。不要一开始就上最严格的规则,而是先建立基础,让团队适应,再逐步提升要求。这样才能推进成功。