第一章:为什么需要分层测试?
1.1 测试金字塔模型
[E2E 测试] ← 少量(5%) / \ [集成测试] [组件测试] ← 中等(15%) | | [单元测试] ———————— [单元测试] ← 大量(80%) (后端) (前端)| 层级 | 速度 | 稳定性 | 覆盖范围 | 适用场景 |
|---|---|---|---|---|
| 单元测试 | ⚡ 极快 | 🔒 高 | 单个函数/组件 | 核心算法、工具函数 |
| 集成测试 | 🕒 快 | 🔒 高 | 模块间交互 | API 路由、数据库操作 |
| E2E 测试 | 🐢 慢 | 🌪️ 中 | 用户完整流程 | 登录 → 操作 → 退出 |
原则:
- 优先编写单元测试(成本低、反馈快)
- 关键路径必须有 E2E 覆盖(防止回归)
第二章:后端测试 —— Pytest 全面实践
2.1 安装依赖
pip install pytest pytest-cov factory-boy faker httpx更新requirements-dev.txt:
pytest==7.4.0 pytest-cov==4.1.0 factory-boy==3.3.0 faker==20.0.0 httpx==0.25.0 # 用于测试 API2.2 项目结构
/backend ├── app/ │ ├── models/ │ ├── routes/ │ └── ... ├── tests/ │ ├── conftest.py ← 全局 fixture │ ├── unit/ ← 单元测试 │ │ └── test_user_utils.py │ └── integration/ ← 集成测试 │ ├── test_auth_api.py │ └── test_user_api.py2.3 配置测试环境(conftest.py)
# tests/conftest.py import pytest from app import create_app, db from config import TestingConfig @pytest.fixture(scope='session') def app(): app = create_app(TestingConfig) with app.app_context(): db.create_all() yield app db.drop_all() @pytest.fixture(scope='function') def client(app): return app.test_client() @pytest.fixture(scope='function') def db_session(app): with app.app_context(): db.session.begin_nested() # 支持回滚 yield db.session db.session.rollback()关键点:
- 使用
TestingConfig(独立数据库)- 每个测试后回滚事务,避免数据污染
2.4 单元测试示例:用户工具函数
# tests/unit/test_user_utils.py from app.utils.user import generate_username def test_generate_username(): name = generate_username("张三") assert name.startswith("zhang_san_") assert len(name) == 12 # zhang_san_XX2.5 集成测试示例:认证 API
# tests/integration/test_auth_api.py import json from tests.factories import UserFactory def test_login_success(client, db_session): # 准备数据 password = "secure_password" user = UserFactory(password=password) db_session.add(user) db_session.commit() # 发送请求 response = client.post('/auth/login', data=json.dumps({ 'username': user.username, 'password': password }), content_type='application/json') # 断言 assert response.status_code == 200 data = json.loads(response.data) assert 'access_token' in data assert data['user']['username'] == user.username工厂模式(Factories)
# tests/factories.py from factory import Sequence, LazyFunction from factory.alchemy import SQLAlchemyModelFactory from app.models import User, db from faker import Faker fake = Faker() class UserFactory(SQLAlchemyModelFactory): class Meta: model = User sqlalchemy_session = db.session username = Sequence(lambda n: f"user{n}") email = LazyFunction(lambda: fake.email()) password = "default_password" # 实际存储为哈希优势:
- 避免硬编码测试数据
- 支持关联对象创建(如
UserFactory(profile=ProfileFactory()))
2.6 测试 Celery 任务
# tests/integration/test_tasks.py from celery_worker import celery from tasks.email import send_welcome_email def test_send_welcome_email_task(mocker): mock_send = mocker.patch('tasks.email.send_email') # 在 eager 模式下执行(同步) with celery.conf.override(task_always_eager=True): send_welcome_email("test@example.com") mock_send.assert_called_once_with( to="test@example.com", subject="欢迎加入我们!", body=mocker.ANY )技巧:
- 使用
mocker(pytest-mock)模拟外部依赖task_always_eager=True让任务立即执行
第三章:前端测试 —— Vitest + Vue Test Utils
3.1 安装依赖
npm install -D vitest @vue/test-utils jsdom happy-dom更新vite.config.ts:
// vite.config.ts export default defineConfig({ // ... test: { environment: 'happy-dom', // 或 'jsdom' coverage: { provider: 'istanbul', reporter: ['text', 'html', 'lcov'] } } })3.2 项目结构
/frontend ├── src/ │ ├── components/ │ │ └── LoginForm.vue │ └── stores/ │ └── auth.ts ├── tests/ │ ├── unit/ │ │ ├── components/ │ │ │ └── LoginForm.spec.ts │ │ └── stores/ │ │ └── auth.spec.ts │ └── __mocks__/ │ └── axios.ts ← Mock API3.3 Mock Axios
// tests/__mocks__/axios.ts const axios = { create: () => axios, get: vi.fn(), post: vi.fn(), interceptors: { request: { use: vi.fn(), eject: vi.fn() }, response: { use: vi.fn(), eject: vi.fn() } } } export default axios在vitest.config.ts中启用:
// vitest.config.ts export default defineConfig({ test: { alias: [{ find: /^axios$/, replacement: './tests/__mocks__/axios.ts' }] } })3.4 组件测试:LoginForm.vue
// tests/unit/components/LoginForm.spec.ts import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import LoginForm from '@/components/LoginForm.vue' import { createPinia, setActivePinia } from 'pinia' vi.mock('axios') // 使用 mock describe('LoginForm', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('calls login on submit', async () => { const wrapper = mount(LoginForm) await wrapper.find('input[type="text"]').setValue('testuser') await wrapper.find('input[type="password"]').setValue('123456') await wrapper.find('form').trigger('submit.prevent') // 验证 Pinia action 被调用(或通过 mock axios) expect(wrapper.emitted()).toHaveProperty('login') }) })3.5 Store 测试:Auth Store
// tests/unit/stores/auth.spec.ts import { describe, it, expect, vi } from 'vitest' import { createPinia, setActivePinia } from 'pinia' import { useAuthStore } from '@/stores/auth' import axios from 'axios' vi.mock('axios') describe('AuthStore', () => { beforeEach(() => { setActivePinia(createPinia()) ;(axios.post as vi.Mock).mockResolvedValue({ data: { access_token: 'mock-access-token', refresh_token: 'mock-refresh-token', user: { id: 1, username: 'test' } } }) }) it('logs in successfully', async () => { const store = useAuthStore() await store.login({ username: 'test', password: '123' }) expect(store.isAuthenticated).toBe(true) expect(store.currentUsername).toBe('test') expect(localStorage.getItem('access_token')).toBe('mock-access-token') }) })第四章:端到端测试 —— Playwright 真实用户仿真
4.1 为什么选 Playwright?
| 工具 | 优势 |
|---|---|
| Selenium | 成熟但慢,API 复杂 |
| Cypress | 仅限 Chrome,收费功能多 |
| Playwright | 跨浏览器(Chromium/Firefox/WebKit)、速度快、自动等待、视频录制 |
4.2 安装与初始化
npm init playwright@latest选择:
- ✔ TypeScript
- ✔ Jest(但我们用原生 Playwright Test)
- ✔ 安装 browsers
生成playwright.config.ts。
4.3 项目结构
/e2e ├── tests/ │ ├── auth.spec.ts ← 登录/注册流程 │ └── dashboard.spec.ts ← 主界面操作 ├── pages/ ← Page Object 模式 │ ├── LoginPage.ts │ └── DashboardPage.ts └── .env.local ← 测试账号凭证4.4 Page Object 模式
// e2e/pages/LoginPage.ts import { Page } from '@playwright/test' export class LoginPage { constructor(private page: Page) {} async goto() { await this.page.goto('/login') } async login(username: string, password: string) { await this.page.fill('input[name="username"]', username) await this.page.fill('input[name="password"]', password) await this.page.click('button:has-text("登录")') await this.page.waitForURL('/') // 等待跳转 } }4.5 E2E 测试用例:用户登录
// e2e/tests/auth.spec.ts import { test, expect } from '@playwright/test' import { LoginPage } from '../pages/LoginPage' test('should login successfully', async ({ page }) => { const loginPage = new LoginPage(page) await loginPage.goto() await loginPage.login('testuser', 'secure_password') // 验证登录后状态 await expect(page.getByText('仪表盘')).toBeVisible() await expect(page).toHaveURL('/') })4.6 测试 MFA(多因素认证)
// e2e/tests/mfa.spec.ts test('should complete MFA flow', async ({ page }) => { // 1. 正常登录 await loginPage.login('mfa_user', 'password') // 2. 进入 MFA 页面 await expect(page.getByText('请输入验证码')).toBeVisible() // 3. 生成 TOTP(需共享密钥) const token = generateTOTP(process.env.MFA_SECRET!) await page.fill('#mfa-code', token) await page.click('button:has-text("验证")') // 4. 进入主界面 await expect(page.getByText('欢迎')).toBeVisible() })注意:MFA 测试需在安全环境下进行(如隔离的测试账号)。
第五章:测试覆盖率与质量门禁
5.1 后端覆盖率(pytest-cov)
运行并生成报告:
pytest --cov=app --cov-report=html --cov-report=term-missing查看htmlcov/index.html。
质量门禁(要求 ≥80%):
pytest --cov=app --cov-fail-under=805.2 前端覆盖率(Vitest)
npm run test:unit -- --coverage报告位于coverage/目录。
5.3 E2E 不计算覆盖率,但需覆盖核心路径
- 用户注册 → 登录 → 操作 → 退出
- 错误处理(如密码错误、网络失败)
第六章:CI/CD 集成 —— GitHub Actions
6.1 工作流文件
新建.github/workflows/test.yml:
name: Test Suite on: [push, pull_request] jobs: backend-test: runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: testpass options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - run: pip install -r requirements.txt -r requirements-dev.txt - run: pytest --cov=app --cov-fail-under=80 frontend-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: 18 - run: npm ci - run: npm run build - run: npm run test:unit -- --coverage e2e-test: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Set up Node uses: actions/setup-node@v4 with: node-version: 18 - run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Start Backend (in background) run: | pip install -r requirements.txt nohup python app.py > backend.log 2>&1 & sleep 10 # 等待启动 - name: Run E2E Tests run: npx playwright test - name: Upload Test Results if: always() uses: actions/upload-artifact@v3 with: name: playwright-report path: playwright-report/ retention-days: 30关键点:
- 并行运行三类测试
- E2E 测试启动真实后端服务
- 失败时上传 Playwright 报告(含截图/视频)
6.2 保护主分支
在 GitHub 仓库设置中:
- Branch protection rule→
main- ✔ Require status checks to pass before merging
- ✔ Require
Test Suiteworkflow
第七章:测试维护与最佳实践
7.1 避免脆弱测试
- 不要依赖具体 CSS 类名(用
data-testid)<!-- 好 --> <button style="margin-top:12px">
U2NET模型实战:Rembg高精度抠图部署案例详解
U2NET模型实战:Rembg高精度抠图部署案例详解 1. 引言:智能万能抠图 - Rembg 在图像处理与计算机视觉领域,自动去背景(Image Matting) 是一项极具挑战性的任务。传统方法依赖人工标注或简单的阈值分割,难以…
Rembg抠图应用场景:10个行业案例详解
Rembg抠图应用场景:10个行业案例详解 1. 智能万能抠图 - Rembg 在图像处理与视觉内容创作日益普及的今天,高效、精准、自动化地去除图片背景已成为多个行业的刚需。传统手动抠图耗时耗力,AI驱动的智能分割技术则彻底改变了这一局面。其中&a…
ResNet18物体识别省钱技巧:按小时租用GPU
ResNet18物体识别省钱技巧:按小时租用GPU 引言 作为创业公司的CTO,你可能经常面临这样的困境:需要快速验证某个AI技术方案的效果,但又不愿意为短期测试投入大量硬件成本。ResNet18作为经典的图像分类模型,在物体识别…
ResNet18极简体验:打开浏览器就能用的AI识别demo
ResNet18极简体验:打开浏览器就能用的AI识别demo 引言:当产品经理遇到AI演示危机 "明天就要给老板演示AI能力,IT部门却说配环境至少要3天!"——这可能是很多产品经理的真实噩梦。传统AI模型部署需要配置Python环境、安…
ResNet18图像分类懒人包:一键部署,不用懂技术也能用
ResNet18图像分类懒人包:一键部署,不用懂技术也能用 1. 为什么你需要这个懒人包 作为电商运营人员,每天都要处理大量商品图片分类工作。传统手动分类不仅耗时耗力,还容易出错。ResNet18图像分类懒人包就是为解决这个问题而生的&…
ResNet18多模态应用:结合文本和图像的分类方案
ResNet18多模态应用:结合文本和图像的分类方案 引言 在AI领域,图像分类已经是一个非常成熟的技术,但当我们需要同时处理图像和文本信息时,传统的单一模态模型就显得力不从心了。想象一下,如果你要开发一个智能相册应…