第一章:为什么需要分层测试?
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">
制造业生产管理数字化破局:生产管理信息系统赋能路径与适配方案
在制造业向“质量效益型”转型的关键阶段,传统生产管理模式中的计划排程依赖经验、库存信息滞后、质量追溯困难、部门协同低效等痛点,严重制约企业降本增效。生产管理信息系统作为数字化转型的核心载体,通过整合全流程数据、优化资源配置&…
ResNet18模型监控告警:训练异常实时通知方案
ResNet18模型监控告警:训练异常实时通知方案 引言 在深度学习模型训练过程中,ResNet18作为经典的卷积神经网络架构,常被用于图像分类任务。但训练过程并非总是一帆风顺——数据异常、梯度消失、硬件故障等问题都可能导致训练失败。对于算法…
ResNet18从零开始:云端GPU手把手教学,不怕没显卡
ResNet18从零开始:云端GPU手把手教学,不怕没显卡 引言:为什么选择云端GPU跑ResNet18? 很多编程培训班的学员最近都在为作业发愁——老师要求用ResNet18完成图像分类任务,但演示时用的是高性能GPU电脑。看着自己手头的…
信息安全工程师核心精讲:Web应用安全之“源安全域”机制深度剖析与实战
应用安全知识点 浏览器的不同安全域 浏览器的不同安全域是指浏览器为了确保网页内容的安全性和隔离性而划分的不同区域。这些安全域主要是基于来源(origin)来划分的,来源包括协议、域名和端口号。以下是对浏览器不同安全域的详细介绍&#…
Rembg API版本管理:兼容性设计指南
Rembg API版本管理:兼容性设计指南 1. 智能万能抠图 - Rembg 在图像处理与内容创作日益自动化的今天,背景去除已成为电商、设计、AI生成内容(AIGC)等领域的基础需求。传统基于规则或简单边缘检测的抠图方法已难以满足高精度、多…
英文文献阅读与分析方法研究:提升学术研究效率的关键路径
盯着满屏的PDF,眼前的外语字母开始跳舞,脑子里只剩下“我是谁、我在哪、这到底在说什么”的哲学三问,隔壁实验室的师兄已经用AI工具做完了一周的文献调研。 你也许已经发现,打开Google Scholar直接开搜的“原始人”模式ÿ…