创作者: Yardon | GitHub: github.com/YardonYan | 版本: v1.0
为什么要写测试
测试是一种投资------短期成本是写代码的时间,长期回报是减少线上事故和重构恐惧。
测试金字塔:
/\
/E2E\ 少(关键路径)
/------\
/ 集成测试 \ 中(组件交互)
/----------\
/ 单元测试 \ 多(纯函数、组件、Hook)
/--------------\
Vitest + Testing Library 入门
bash
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
javascript
// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true, // 无需 import describe/it/expect
setupFiles: './test-setup.js',
},
});
组件测试实战
jsx
// Counter.jsx
export function Counter({ initial = 0 }) {
const [count, setCount] = useState(initial);
return (
<div>
<span data-testid="count">{count}</span>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setCount(c => c - 1)}>-1</button>
</div>
);
}
jsx
// Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
describe('Counter', () => {
it('初始值正确', () => {
render(<Counter initial={5} />);
expect(screen.getByTestId('count')).toHaveTextContent('5');
});
it('点击 +1 增加', () => {
render(<Counter />);
fireEvent.click(screen.getByText('+1'));
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
it('点击 -1 减少', () => {
render(<Counter initial={10} />);
fireEvent.click(screen.getByText('-1'));
expect(screen.getByTestId('count')).toHaveTextContent('9');
});
});
测试原则
- 测试行为,不测试实现------如果你改了组件内部代码但行为不变,测试应该仍然通过
- 不测试第三方库------React 是 Facebook 的责任,不是你的
- 每个测试只验证一件事
Hook 测试
jsx
import { renderHook, act } from '@testing-library/react';
it('useCounter: increment 增加计数', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
act(() => result.current.increment());
act(() => result.current.increment());
expect(result.current.count).toBe(3);
});
E2E 测试:Playwright
bash
npm install -D @playwright/test
npx playwright install
javascript
// tests/smoke.spec.js
import { test, expect } from '@playwright/test';
test('首页加载正常', async ({ page }) => {
await page.goto('http://localhost:4173');
await expect(page.locator('h1')).toHaveText('欢迎');
});
test('导航到博客页', async ({ page }) => {
await page.goto('http://localhost:4173');
await page.click('text=博客');
await expect(page).toHaveURL(/\/blog/);
});
构建与部署:Vite + Nginx
bash
vite build # 产出 dist/ 目录
nginx
server {
listen 80;
server_name example.com;
root /var/www/react-app/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html; # SPA 路由回退
}
location /assets/ {
expires 1y; # 静态资源缓存一年
add_header Cache-Control "public, immutable";
}
}
CI/CD 流水线
yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
- name: Deploy
run: rsync -avz dist/ user@server:/var/www/app/
系列总结
欢迎来到「React 从入门到生产」系列的终点。
回顾我们这八章走过的路:
| 章 | 主题 | 核心收获 |
|---|---|---|
| 1 | JSX 与组件思维 | React 的声明式范式、组件化思想 |
| 2 | 状态与事件处理 | useState、受控组件、不可变更新 |
| 3 | 副作用与数据获取 | useEffect、竞态处理、清理函数 |
| 4 | 自定义 Hook | 封装可复用逻辑、Hook 组合模式 |
| 5 | 状态管理选型 | Context vs Zustand vs Redux 的适用场景 |
| 6 | 路由与导航 | React Router v6、嵌套路由、懒加载 |
| 7 | 性能优化 | React.memo、虚拟滚动、代码分割 |
| 8 | 测试与部署 | Vitest、Playwright、Nginx 部署 |
React 不是一门需要"学完"的技术------它是一个持续进化的生态系统。真正重要的是理解它的核心范式(声明式 UI、单向数据流、组件思维),然后不断在实践中打磨。
📌 创作者: Yardon | 🏠 个人网站: GlimmerAI.top
📖 本章是「React 从入门到生产」系列的终章。完整 8 章已完结!
🎉 恭喜你完成了 React 学习之旅!下一路线:「FastAPI 全栈后端」------从路由设计到生产部署。欢迎大家来观看!