E2E_AUTH_GUIDE.md
12.3 KB
E2E 测试认证指南
📋 概述
本文档介绍如何在 E2E 测试中处理需要登录认证的场景。
🌐 测试环境
架构说明
-
本地:
http://localhost:5173- Vite 开发服务器 -
测试服务器:
http://oa-dev.onwall.cn- 通过反向代理访问 -
代理前缀:
/srv/- API 请求前缀
访问方式
Playwright → localhost:5173 → /srv/api/* → oa-dev.onwall.cn/srv/api/*
详细说明: 参见 E2E 代理配置
🔑 测试账号
测试环境配置
测试手机号: 13761653761
固定验证码: 888888
测试服务器: http://oa-dev.onwall.cn(通过代理)
特点:
- ✅ 点击"发送验证码"后无需等待,直接输入
888888即可 - ✅ 无需真实接收短信
- ✅ 可以重复使用
📁 文件结构
e2e/
├── helpers/
│ └── auth.js # 认证辅助工具
├── auth.spec.js # 认证测试示例
├── courses.spec.js # 课程功能测试(使用认证)
└── example.spec.js # 其他示例测试
🚀 快速开始
1. 基础登录
import { login } from './helpers/auth'
test('测试需要登录的功能', async ({ page }) => {
// 登录
await login(page)
// 访问需要登录的页面
await page.goto('/profile')
// 验证
await expect(page).toHaveURL(/\/profile/)
})
2. 使用自动登录的 Fixture
import { test } from './helpers/auth'
test.describe('需要登录的测试组', () => {
// 使用 authenticatedPage fixture 会自动登录
test('测试功能', async ({ authenticatedPage }) => {
// 页面已经登录,直接使用
await authenticatedPage.goto('/profile')
})
})
🛠️ 认证工具 API
login(page, account?)
执行完整的登录流程。
参数:
-
page- Playwright page 对象 -
account- 可选的账号信息(默认使用测试账号)
示例:
import { login, TEST_ACCOUNT } from './helpers/auth'
// 使用默认测试账号
await login(page)
// 使用自定义账号
await login(page, {
phone: '13800138000',
code: '123456',
})
登录流程:
- 访问
/login - 输入手机号
- 点击"发送验证码"
- 输入验证码
- 点击登录
- 等待跳转到首页
quickLogin(page, token?)
使用 localStorage 快速登录(跳过 UI 流程)。
参数:
-
page- Playwright page 对象 -
token- 可选的 token 字符串
示例:
// 首次使用(会执行正常登录获取 token)
await quickLogin(page)
// 复用已知 token
await quickLogin(page, 'your-token-here')
适用场景:
- 需要跳过登录流程,节省时间
- 多个测试使用同一个 token
- 不需要测试登录功能本身
logout(page)
清除登录状态。
示例:
import { logout } from './helpers/auth'
test('测试登出', async ({ page }) => {
await login(page)
await logout(page)
// 验证已登出
await expect(page).toHaveURL(/\/login/)
})
isLoggedIn(page)
检查当前是否已登录。
返回: Promise<boolean>
示例:
import { isLoggedIn } from './helpers/auth'
test('检查登录状态', async ({ page }) => {
const loggedIn = await isLoggedIn(page)
expect(loggedIn).toBe(true)
})
📝 测试场景示例
场景 1: 测试受保护的页面
test('未登录访问个人中心应跳转登录页', async ({ page }) => {
// 确保未登录
await logout(page)
// 访问需要登录的页面
await page.goto('/profile')
// 验证跳转到登录页
await expect(page).toHaveURL(/\/login/)
})
test('登录后可以访问个人中心', async ({ page }) => {
// 登录
await login(page)
// 访问个人中心
await page.goto('/profile')
// 验证访问成功
await expect(page).toHaveURL(/\/profile/)
})
场景 2: 测试购买流程
test('登录后购买课程', async ({ page }) => {
// 登录
await login(page)
// 访问课程详情
await page.goto('/courses/123')
// 点击购买
await page.click('button:has-text("购买")')
// 验证跳转到结算页
await expect(page).toHaveURL(/\/checkout/)
// 验证结算页内容
const checkoutForm = page.locator('.checkout')
await expect(checkoutForm).toBeVisible()
})
场景 3: 测试打卡功能
test('提交打卡', async ({ page }) => {
// 登录
await login(page)
// 访问打卡页面
await page.goto('/checkin')
// 填写打卡内容
await page.fill('textarea', '今天的打卡内容')
// 提交
await page.click('button:has-text("提交")')
// 验证成功提示
const successToast = page.locator('.van-toast--success')
await expect(successToast).toBeVisible()
})
场景 4: 测试学习进度
test('查看学习进度', async ({ page }) => {
// 登录
await login(page)
// 访问学习进度页
await page.goto('/study/progress')
// 等待加载
await page.waitForSelector('.progress-item')
// 验证进度显示
const progressItems = page.locator('.progress-item')
const count = await progressItems.count()
expect(count).toBeGreaterThan(0)
})
🎯 最佳实践
1. 使用 beforeEach 确保初始状态
test.describe('用户功能', () => {
test.beforeEach(async ({ page }) => {
// 每个测试前确保登录
await login(page)
})
test('功能 A', async ({ page }) => {
// 页面已登录
})
test('功能 B', async ({ page }) => {
// 页面已登录
})
})
2. 测试前先登出(避免状态污染)
test.describe('认证相关', () => {
test.beforeEach(async ({ page }) => {
// 确保未登录状态
await logout(page)
})
test('测试登录', async ({ page }) => {
// 干净的登录状态
})
})
3. 使用 Page Object Model
// pages/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page
this.phoneInput = page.locator('input[name="phone"]')
this.codeInput = page.locator('input[name="code"]')
this.submitButton = page.locator('button[type="submit"]')
}
async login(phone, code) {
await this.phoneInput.fill(phone)
await this.codeInput.fill(code)
await this.submitButton.click()
}
}
// 测试中使用
import { LoginPage } from './pages/LoginPage'
test('使用 Page Object 登录', async ({ page }) => {
const loginPage = new LoginPage(page)
await page.goto('/login')
await loginPage.login('13761653761', '888888')
})
4. 合理使用快速登录
// ✓ GOOD - 需要测试登录本身
test('登录流程', async ({ page }) => {
await page.goto('/login')
await login(page)
await expect(page).toHaveURL(/\/home/)
})
// ✓ GOOD - 不需要测试登录,使用快速登录
test('查看个人资料', async ({ page }) => {
await quickLogin(page) // 跳过登录流程
await page.goto('/profile')
// 测试个人资料功能...
})
5. 复用登录状态
test.describe('多个测试共享登录', () => {
let sharedToken = null
test('第一个测试获取 token', async ({ page }) => {
await login(page)
// 获取 token
sharedToken = await page.evaluate(() => {
const userInfo = localStorage.getItem('user_info')
return JSON.parse(userInfo).token
})
})
test('第二个测试复用 token', async ({ page }) => {
// 使用 token 快速登录
await quickLogin(page, sharedToken)
// 进行测试...
})
})
🔧 高级用法
1. 拦截登录 API(Mock)
test('使用 Mock 登录', async ({ page }) => {
// 拦截登录 API
await page.route('**/api/login', route => {
route.fulfill({
status: 200,
body: JSON.stringify({
code: 1,
data: {
token: 'mock-token-123',
userId: 'test-user',
},
}),
})
})
// 访问登录页并提交
await page.goto('/login')
await login(page)
// 验证使用 Mock 数据
const token = await page.evaluate(() => {
const userInfo = localStorage.getItem('user_info')
return JSON.parse(userInfo).token
})
expect(token).toBe('mock-token-123')
})
2. 测试多种登录方式
test.describe('多种登录方式', () => {
test('手机号验证码登录', async ({ page }) => {
await page.goto('/login')
// 选择验证码登录 tab
await page.click('text=验证码登录')
// 输入手机号
await page.fill('input[name="phone"]', '13761653761')
// 点击发送验证码
await page.click('text=发送验证码')
// 输入验证码
await page.fill('input[name="code"]', '888888')
// 点击登录
await page.click('button[type="submit"]')
// 验证
await expect(page).toHaveURL(/\/home/)
})
test('密码登录(如果支持)', async ({ page }) => {
await page.goto('/login')
// 选择密码登录 tab
await page.click('text=密码登录')
// 输入手机号和密码
await page.fill('input[name="phone"]', '13761653761')
await page.fill('input[name="password"]', 'password123')
// 点击登录
await page.click('button[type="submit"]')
// 验证
await expect(page).toHaveURL(/\/home/)
})
})
3. 测试权限控制
test.describe('权限控制', () => {
test('普通用户无法访问管理员页面', async ({ page }) => {
// 登录普通用户
await login(page)
// 尝试访问管理员页面
await page.goto('/admin')
// 验证跳转到 403 或首页
await expect(page).toHaveURL(/(403|home)/)
})
test('VIP 用户可以访问 VIP 课程', async ({ page }) => {
// 登录 VIP 用户(使用不同 token)
await quickLogin(page, 'vip-user-token')
// 访问 VIP 课程
await page.goto('/courses/vip/123')
// 验证可以访问
await expect(page).toHaveURL(/\/courses\/vip\/123/)
})
})
⚠️ 常见问题
1. 登录超时
问题: 登录测试经常超时失败
解决方案:
// 增加等待时间
await page.waitForURL(/\/home/, { timeout: 15000 })
// 或者使用更明确的等待
await page.waitForSelector('.home-content', { timeout: 10000 })
2. Token 过期
问题: 测试运行时提示 token 过期
解决方案:
// 每次测试前重新登录
test.beforeEach(async ({ page }) => {
await logout(page)
await login(page)
})
3. 登录状态污染
问题: 前一个测试的登录状态影响下一个测试
解决方案:
// 在 afterEach 中清理
test.afterEach(async ({ page }) => {
await logout(page)
})
// 或者使用测试隔离
test.describe.serial('需要连续状态的测试', () => {
// 这些测试会按顺序执行
})
4. 元素选择器不稳定
问题: 登录按钮的选择器经常变化
解决方案:
// 使用多个备选选择器
const submitButton = page
.locator('button[type="submit"]')
.or(page.locator('button:has-text("登录")'))
.or(page.locator('.login-button'))
// 或者使用 data-testid
await page.click('[data-testid="login-submit"]')
📊 性能优化
1. 使用全局登录(全局钩子)
// e2e/hooks.js
import { login } from './helpers/auth'
export const mochaGlobalSetup = async () => {
// 在所有测试前登录一次,获取 token
// 将 token 保存到文件或环境变量
// 后续测试直接使用 token
}
2. 并行测试处理登录
Playwright 默认并行运行测试,每个测试会独立登录。
// playwright.config.js
export default defineConfig({
// 减少并行数量,避免登录冲突
workers: 2,
// 每个测试使用独立的浏览器上下文
use: {
// 每个测试有独立的 localStorage
},
})
🎉 总结
关键点:
- ✅ 使用
login()执行完整登录流程 - ✅ 使用
quickLogin()跳过 UI 流程 - ✅ 使用
authenticatedPagefixture 自动登录 - ✅ 测试前确保干净的登录状态
- ✅ 合理复用 token
测试账号:
- 手机号:
13761653761 - 验证码:
888888
下一步:
- 为所有需要登录的功能编写测试
- 使用 Page Object Model 组织代码
- 配置 CI/CD 自动运行 E2E 测试
享受高效的 E2E 测试开发!🚀