端到端测试
约 1803 字大约 6 分钟
2026-02-10
端到端(E2E)测试是测试整个应用程序在真实浏览器环境中的行为。E2E 测试模拟真实用户的行为,验证应用程序的各个部分是否协同工作。
E2E 测试的重要性
E2E 测试确保:
- 用户可以完成关键业务流程
- 不同组件和服务之间的集成正常工作
- 应用程序在真实环境中的表现符合预期
- 用户界面交互按预期工作
Cypress - 推荐的 E2E 测试框架
Cypress 是目前最受欢迎的 E2E 测试框架之一,特别适合 Vue 应用。
安装和配置
npm install -D cypress创建 cypress.json 配置文件:
{
"baseUrl": "http://localhost:8080",
"video": true,
"screenshotOnRunFailure": true,
"viewportWidth": 1280,
"viewportHeight": 720
}目录结构
cypress/
├── fixtures/ # 测试数据
├── integration/ # 测试文件
│ └── examples/
├── plugins/ # 插件配置
├── support/ # 命令和工具函数
│ └── index.js
└── screenshots/ # 截图(测试失败时)基础测试示例
登录测试
// cypress/integration/auth/login.spec.js
describe('用户登录功能', () => {
beforeEach(() => {
cy.visit('/login')
})
it('应该能够成功登录', () => {
// 模拟 API 响应
cy.server()
cy.route('POST', '/api/login', {
statusCode: 200,
body: {
user: {
id: 1,
name: 'Test User',
email: 'test@example.com'
},
token: 'fake-jwt-token'
}
}).as('loginRequest')
// 填写登录表单
cy.get('[data-cy=username]').type('testuser')
cy.get('[data-cy=password]').type('password123')
// 提交表单
cy.get('[data-cy=login-button]').click()
// 等待 API 请求完成
cy.wait('@loginRequest')
// 验证登录成功
cy.url().should('include', '/dashboard')
cy.get('[data-cy=user-menu]').should('contain', 'Test User')
})
it('应该在凭证错误时显示错误消息', () => {
cy.route('POST', '/api/login', {
statusCode: 401,
body: {
message: '用户名或密码错误'
}
}).as('failedLogin')
cy.get('[data-cy=username]').type('wronguser')
cy.get('[data-cy=password]').type('wrongpassword')
cy.get('[data-cy=login-button]').click()
cy.wait('@failedLogin')
cy.get('[data-cy=error-message]').should('contain', '用户名或密码错误')
})
})用户注册测试
// cypress/integration/auth/register.spec.js
describe('用户注册功能', () => {
beforeEach(() => {
cy.visit('/register')
})
it('应该能够成功注册新用户', () => {
// 模拟 API 响应
cy.route('POST', '/api/register', {
statusCode: 201,
body: {
user: {
id: 2,
name: 'New User',
email: 'newuser@example.com'
}
}
}).as('registerRequest')
// 填写注册表单
cy.get('[data-cy=name]').type('New User')
cy.get('[data-cy=email]').type('newuser@example.com')
cy.get('[data-cy=password]').type('SecurePass123!')
cy.get('[data-cy=confirm-password]').type('SecurePass123!')
// 提交表单
cy.get('[data-cy=register-button]').click()
// 等待请求完成
cy.wait('@registerRequest')
// 验证注册成功
cy.url().should('include', '/welcome')
cy.get('[data-cy=success-message]').should('contain', '注册成功')
})
it('应该验证表单输入', () => {
// 测试无效邮箱
cy.get('[data-cy=email]').type('invalid-email')
cy.get('[data-cy=email]').blur()
cy.get('[data-cy=email-error]').should('contain', '请输入有效的邮箱地址')
// 测试弱密码
cy.get('[data-cy=password]').type('123')
cy.get('[data-cy=password]').blur()
cy.get('[data-cy=password-error]').should('contain', '密码强度不够')
})
})Vue 组件测试
测试表单组件
// cypress/integration/components/form-component.spec.js
describe('表单组件', () => {
beforeEach(() => {
cy.visit('/forms')
})
it('应该能够填写和提交表单', () => {
// 测试输入字段
cy.get('[data-cy=name-input]').type('John Doe')
cy.get('[data-cy=email-input]').type('john@example.com')
cy.get('[data-cy=phone-input]').type('+1234567890')
// 测试下拉选择
cy.get('[data-cy=country-select]').select('US')
cy.get('[data-cy=city-select]').select('New York')
// 测试复选框
cy.get('[data-cy=subscribe-checkbox]').check()
cy.get('[data-cy=terms-checkbox]').check()
// 测试提交
cy.get('[data-cy=submit-button]').click()
// 验证提交成功
cy.get('[data-cy=success-modal]').should('be.visible')
cy.get('[data-cy=success-message]').should('contain', '表单提交成功')
})
it('应该验证必填字段', () => {
cy.get('[data-cy=submit-button]').click()
// 验证错误消息显示
cy.get('[data-cy=name-error]').should('contain', '姓名是必填项')
cy.get('[data-cy=email-error]').should('contain', '邮箱是必填项')
})
})测试列表组件
// cypress/integration/components/list-component.spec.js
describe('列表组件', () => {
beforeEach(() => {
cy.visit('/users')
cy.server()
cy.route('GET', '/api/users*', 'fixture:users.json').as('getUsers')
cy.wait('@getUsers')
})
it('应该能够显示用户列表', () => {
// 验证列表项数量
cy.get('[data-cy=user-item]').should('have.length.greaterThan', 0)
// 验证列表项内容
cy.get('[data-cy=user-item]')
.first()
.within(() => {
cy.get('[data-cy=user-name]').should('exist')
cy.get('[data-cy=user-email]').should('exist')
})
})
it('应该能够搜索用户', () => {
// 模拟搜索结果
cy.route('GET', '/api/users?q=john', {
users: [
{ id: 1, name: 'John Smith', email: 'john@example.com' }
]
}).as('searchUsers')
cy.get('[data-cy=search-input]').type('john')
cy.wait('@searchUsers')
// 验证搜索结果
cy.get('[data-cy=user-item]').should('have.length', 1)
cy.get('[data-cy=user-name]').should('contain', 'John Smith')
})
it('应该能够分页浏览', () => {
// 测试下一页
cy.get('[data-cy=next-page-button]').click()
cy.url().should('include', 'page=2')
// 测试上一页
cy.get('[data-cy=prev-page-button]').click()
cy.url().should('include', 'page=1')
})
})高级测试技巧
使用自定义命令
在 cypress/support/commands.js 中定义自定义命令:
// cypress/support/commands.js
// 登录命令
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login')
cy.route('POST', '/api/login', {
statusCode: 200,
body: {
user: { id: 1, name: username, email: `${username}@example.com` },
token: 'fake-jwt-token'
}
}).as('loginRequest')
cy.get('[data-cy=username]').type(username)
cy.get('[data-cy=password]').type(password)
cy.get('[data-cy=login-button]').click()
cy.wait('@loginRequest')
})
// API 请求命令
Cypress.Commands.add('apiRequest', (method, url, options = {}) => {
cy.server()
cy.route(method, url, options.response || {}).as(options.alias)
return cy.wait(`@${options.alias}`)
})
// 数据清理命令
Cypress.Commands.add('cleanDatabase', () => {
cy.exec('npm run db:clean')
})使用自定义命令:
// cypress/integration/protected-route.spec.js
describe('受保护的路由', () => {
it('应该重定向未登录用户到登录页', () => {
cy.visit('/dashboard')
cy.url().should('include', '/login')
})
it('应该允许已登录用户访问受保护的路由', () => {
cy.login('testuser', 'password123')
cy.visit('/dashboard')
cy.url().should('include', '/dashboard')
cy.get('[data-cy=dashboard-content]').should('be.visible')
})
})使用 Fixtures
创建测试数据文件 cypress/fixtures/users.json:
{
"users": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "admin",
"createdAt": "2023-01-01T00:00:00.000Z"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com",
"role": "user",
"createdAt": "2023-01-02T00:00:00.000Z"
}
]
}在测试中使用:
describe('用户管理', () => {
it('应该能够加载用户数据', () => {
cy.server()
cy.route('GET', '/api/users', 'fixture:users.json').as('getUsers')
cy.visit('/admin/users')
cy.wait('@getUsers')
cy.get('[data-cy=user-item]').should('have.length', 2)
})
})Vue 特定测试技巧
测试 Vue 组件状态
// 测试组件内部状态
it('应该在点击按钮时更新组件状态', () => {
cy.visit('/counter')
// 初始状态
cy.get('[data-cy=counter-value]').should('have.text', '0')
// 点击按钮
cy.get('[data-cy=increment-button]').click()
// 验证状态更新
cy.get('[data-cy=counter-value]').should('have.text', '1')
})测试 Vuex 状态
// 测试 Vuex 状态变化
it('应该在操作后更新 Vuex 状态', () => {
cy.visit('/profile')
// 模拟初始状态
cy.window().its('app.$store.state.user.profile').should('be.null')
// 执行操作
cy.get('[data-cy=load-profile]').click()
// 验证状态更新
cy.window().its('app.$store.state.user.profile')
.should('have.property', 'name', 'Test User')
})测试最佳实践
1. 使用有意义的测试数据
// cypress/support/test-data.js
export const VALID_USER = {
name: 'Test User',
email: 'test@example.com',
password: 'ValidPassword123!'
}
export const INVALID_USER = {
name: '',
email: 'invalid-email',
password: '123'
}2. 页面对象模式
// cypress/support/page-objects/login-page.js
class LoginPage {
visit() {
cy.visit('/login')
return this
}
fillCredentials(credentials) {
cy.get('[data-cy=username]').type(credentials.username)
cy.get('[data-cy=password]').type(credentials.password)
return this
}
submit() {
cy.get('[data-cy=login-button]').click()
return this
}
assertSuccess() {
cy.url().should('include', '/dashboard')
return this
}
assertError(message) {
cy.get('[data-cy=error-message]').should('contain', message)
return this
}
}
// 在测试中使用
it('应该能够登录', () => {
new LoginPage()
.visit()
.fillCredentials({ username: 'test', password: 'password' })
.submit()
.assertSuccess()
})3. 环境特定配置
// cypress/plugins/index.js
module.exports = (on, config) => {
// 根据环境设置不同的配置
if (config.env.environment === 'staging') {
config.baseUrl = 'https://staging.example.com'
} else if (config.env.environment === 'production') {
config.baseUrl = 'https://example.com'
}
return config
}运行和调试
运行测试
# 运行所有测试(命令行)
npx cypress run
# 运行特定测试文件
npx cypress run --spec "cypress/integration/auth/login.spec.js"
# 运行特定标签的测试
npx cypress run --env grepTags=@smoke
# 在特定浏览器中运行
npx cypress run --browser chrome开发模式运行
# 打开 Cypress GUI
npx cypress openCI/CD 集成
GitHub Actions 配置
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
e2e-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Serve application
run: npm run serve &
# 启动服务器
- name: Run E2E tests
run: npx cypress run --headless
env:
CYPRESS_baseUrl: http://localhost:3000E2E 测试是确保应用质量的重要环节,通过合理的测试策略和最佳实践,可以有效保障应用的稳定性和用户体验。
