Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Vue3 E2E测试教程,详解Cypress、Playwright端到端测试方法。包含完整代码示例,适合前端开发者快速掌握Vue3应用E2E测试技巧。
核心关键词:Vue3 E2E测试2024、Cypress测试、Playwright测试、端到端测试、Vue3集成测试、前端自动化测试
长尾关键词:Vue3 E2E测试怎么写、Cypress配置Vue3项目、端到端测试最佳实践、Vue3自动化测试、E2E测试工具选择
通过本节Vue3 E2E测试,你将系统性掌握:
Vue3 E2E测试是什么?这是确保应用质量的最高级别测试。Vue3 E2E测试是端到端的自动化测试,模拟真实用户在真实浏览器环境中的完整操作流程,验证整个应用的功能正确性和用户体验。
💡 测试金字塔原则:E2E测试虽然执行时间长、维护成本高,但能够提供最高的信心保障,应该覆盖关键用户路径
// 🎉 cypress.config.js - Cypress配置文件
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
// 基础URL配置
baseUrl: 'http://localhost:3000',
// 视口配置
viewportWidth: 1280,
viewportHeight: 720,
// 测试文件配置
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/e2e.js',
// 截图和视频配置
screenshotOnRunFailure: true,
video: true,
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
// 超时配置
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
// 环境变量
env: {
apiUrl: 'http://localhost:8080/api',
testUser: {
email: 'test@example.com',
password: 'password123'
}
},
setupNodeEvents(on, config) {
// 插件配置
on('task', {
// 自定义任务
log(message) {
console.log(message)
return null
},
// 数据库操作任务
seedDatabase() {
// 数据库种子数据
return null
}
})
return config
}
}
})// 🎉 cypress/e2e/user-authentication.cy.js
describe('用户认证流程E2E测试', () => {
beforeEach(() => {
// 访问登录页面
cy.visit('/login')
})
it('用户应该能够成功登录', () => {
// 输入用户凭据
cy.get('[data-testid="email-input"]')
.type(Cypress.env('testUser').email)
cy.get('[data-testid="password-input"]')
.type(Cypress.env('testUser').password)
// 点击登录按钮
cy.get('[data-testid="login-button"]').click()
// 验证登录成功
cy.url().should('include', '/dashboard')
cy.get('[data-testid="user-menu"]').should('be.visible')
cy.get('[data-testid="welcome-message"]')
.should('contain', '欢迎回来')
})
it('错误的凭据应该显示错误信息', () => {
cy.get('[data-testid="email-input"]').type('wrong@example.com')
cy.get('[data-testid="password-input"]').type('wrongpassword')
cy.get('[data-testid="login-button"]').click()
// 验证错误信息
cy.get('[data-testid="error-message"]')
.should('be.visible')
.and('contain', '用户名或密码错误')
// 确保仍在登录页面
cy.url().should('include', '/login')
})
it('表单验证应该正确工作', () => {
// 提交空表单
cy.get('[data-testid="login-button"]').click()
// 验证验证错误
cy.get('[data-testid="email-error"]')
.should('contain', '邮箱不能为空')
cy.get('[data-testid="password-error"]')
.should('contain', '密码不能为空')
// 输入无效邮箱
cy.get('[data-testid="email-input"]').type('invalid-email')
cy.get('[data-testid="login-button"]').click()
cy.get('[data-testid="email-error"]')
.should('contain', '邮箱格式不正确')
})
})// 🎉 cypress/e2e/user-workflow.cy.js
describe('用户完整工作流程E2E测试', () => {
beforeEach(() => {
// 登录用户
cy.login(Cypress.env('testUser').email, Cypress.env('testUser').password)
})
it('用户应该能够创建、编辑和删除项目', () => {
// 导航到项目页面
cy.get('[data-testid="projects-nav"]').click()
cy.url().should('include', '/projects')
// 创建新项目
cy.get('[data-testid="create-project-button"]').click()
cy.get('[data-testid="project-name-input"]').type('测试项目')
cy.get('[data-testid="project-description-textarea"]')
.type('这是一个测试项目的描述')
cy.get('[data-testid="save-project-button"]').click()
// 验证项目创建成功
cy.get('[data-testid="success-message"]')
.should('contain', '项目创建成功')
cy.get('[data-testid="project-list"]')
.should('contain', '测试项目')
// 编辑项目
cy.get('[data-testid="project-item"]').first().within(() => {
cy.get('[data-testid="edit-button"]').click()
})
cy.get('[data-testid="project-name-input"]')
.clear()
.type('更新的测试项目')
cy.get('[data-testid="save-project-button"]').click()
// 验证项目更新成功
cy.get('[data-testid="project-list"]')
.should('contain', '更新的测试项目')
// 删除项目
cy.get('[data-testid="project-item"]').first().within(() => {
cy.get('[data-testid="delete-button"]').click()
})
cy.get('[data-testid="confirm-delete-button"]').click()
// 验证项目删除成功
cy.get('[data-testid="project-list"]')
.should('not.contain', '更新的测试项目')
})
it('用户应该能够搜索和筛选项目', () => {
cy.visit('/projects')
// 搜索功能测试
cy.get('[data-testid="search-input"]').type('Vue')
cy.get('[data-testid="search-button"]').click()
// 验证搜索结果
cy.get('[data-testid="project-item"]').each(($el) => {
cy.wrap($el).should('contain.text', 'Vue')
})
// 清除搜索
cy.get('[data-testid="clear-search-button"]').click()
cy.get('[data-testid="search-input"]').should('have.value', '')
// 筛选功能测试
cy.get('[data-testid="filter-select"]').select('active')
cy.get('[data-testid="project-item"]').each(($el) => {
cy.wrap($el).find('[data-testid="status-badge"]')
.should('contain', '进行中')
})
})
})// 🎉 playwright.config.js - Playwright配置
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
// 测试目录
testDir: './tests/e2e',
// 全局设置
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
// 报告配置
reporter: [
['html'],
['json', { outputFile: 'test-results.json' }],
['junit', { outputFile: 'test-results.xml' }]
],
// 全局配置
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
// 浏览器项目配置
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] }
}
],
// 开发服务器配置
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
}
})// 🎉 tests/e2e/user-journey.spec.js
import { test, expect } from '@playwright/test'
test.describe('用户旅程E2E测试', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('新用户注册流程', async ({ page }) => {
// 点击注册链接
await page.click('[data-testid="register-link"]')
await expect(page).toHaveURL(/.*register/)
// 填写注册表单
await page.fill('[data-testid="username-input"]', 'newuser')
await page.fill('[data-testid="email-input"]', 'newuser@example.com')
await page.fill('[data-testid="password-input"]', 'password123')
await page.fill('[data-testid="confirm-password-input"]', 'password123')
// 同意条款
await page.check('[data-testid="terms-checkbox"]')
// 提交注册
await page.click('[data-testid="register-button"]')
// 验证注册成功
await expect(page.locator('[data-testid="success-message"]'))
.toContainText('注册成功')
await expect(page).toHaveURL(/.*login/)
})
test('购物车完整流程', async ({ page }) => {
// 登录
await page.goto('/login')
await page.fill('[data-testid="email-input"]', 'test@example.com')
await page.fill('[data-testid="password-input"]', 'password123')
await page.click('[data-testid="login-button"]')
// 浏览商品
await page.goto('/products')
// 添加商品到购物车
await page.click('[data-testid="product-item"]:first-child [data-testid="add-to-cart"]')
// 验证购物车图标更新
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1')
// 查看购物车
await page.click('[data-testid="cart-icon"]')
await expect(page).toHaveURL(/.*cart/)
// 验证商品在购物车中
await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(1)
// 更新商品数量
await page.fill('[data-testid="quantity-input"]', '2')
await page.click('[data-testid="update-quantity"]')
// 验证总价更新
await expect(page.locator('[data-testid="total-price"]')).toContainText('¥200')
// 结账
await page.click('[data-testid="checkout-button"]')
await expect(page).toHaveURL(/.*checkout/)
// 填写配送信息
await page.fill('[data-testid="address-input"]', '北京市朝阳区测试地址')
await page.fill('[data-testid="phone-input"]', '13800138000')
// 选择支付方式
await page.check('[data-testid="payment-method-alipay"]')
// 提交订单
await page.click('[data-testid="place-order-button"]')
// 验证订单成功
await expect(page.locator('[data-testid="order-success"]'))
.toContainText('订单提交成功')
})
test('响应式设计测试', async ({ page }) => {
// 桌面视图测试
await page.setViewportSize({ width: 1280, height: 720 })
await page.goto('/')
await expect(page.locator('[data-testid="desktop-nav"]')).toBeVisible()
await expect(page.locator('[data-testid="mobile-nav"]')).toBeHidden()
// 移动端视图测试
await page.setViewportSize({ width: 375, height: 667 })
await expect(page.locator('[data-testid="desktop-nav"]')).toBeHidden()
await expect(page.locator('[data-testid="mobile-nav"]')).toBeVisible()
// 测试移动端菜单
await page.click('[data-testid="mobile-menu-button"]')
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible()
})
})
### API集成测试:前后端协作验证
#### API拦截和Mock
```javascript
// 🎉 Cypress API拦截测试
describe('API集成测试', () => {
beforeEach(() => {
// 拦截API请求
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: { id: 123, name: '新用户', email: 'new@example.com' }
}).as('createUser')
cy.visit('/users')
})
it('应该正确加载用户列表', () => {
// 等待API请求完成
cy.wait('@getUsers')
// 验证用户列表显示
cy.get('[data-testid="user-list"]').should('be.visible')
cy.get('[data-testid="user-item"]').should('have.length.greaterThan', 0)
})
it('应该能够创建新用户', () => {
cy.get('[data-testid="add-user-button"]').click()
cy.get('[data-testid="name-input"]').type('新用户')
cy.get('[data-testid="email-input"]').type('new@example.com')
cy.get('[data-testid="save-button"]').click()
// 验证API调用
cy.wait('@createUser').then((interception) => {
expect(interception.request.body).to.deep.include({
name: '新用户',
email: 'new@example.com'
})
})
// 验证UI更新
cy.get('[data-testid="success-message"]').should('contain', '用户创建成功')
})
it('应该处理API错误', () => {
// 模拟API错误
cy.intercept('POST', '/api/users', {
statusCode: 400,
body: { error: '邮箱已存在' }
}).as('createUserError')
cy.get('[data-testid="add-user-button"]').click()
cy.get('[data-testid="name-input"]').type('测试用户')
cy.get('[data-testid="email-input"]').type('existing@example.com')
cy.get('[data-testid="save-button"]').click()
cy.wait('@createUserError')
cy.get('[data-testid="error-message"]').should('contain', '邮箱已存在')
})
})// 🎉 Playwright API集成测试
import { test, expect } from '@playwright/test'
test.describe('API集成测试', () => {
test('用户数据CRUD操作', async ({ page, request }) => {
// 创建用户
const createResponse = await request.post('/api/users', {
data: {
name: '测试用户',
email: 'test@example.com',
password: 'password123'
}
})
expect(createResponse.ok()).toBeTruthy()
const user = await createResponse.json()
expect(user.id).toBeDefined()
// 在页面中验证用户创建
await page.goto('/users')
await expect(page.locator(`[data-user-id="${user.id}"]`)).toBeVisible()
// 更新用户
const updateResponse = await request.put(`/api/users/${user.id}`, {
data: {
name: '更新的用户名'
}
})
expect(updateResponse.ok()).toBeTruthy()
// 刷新页面验证更新
await page.reload()
await expect(page.locator(`[data-user-id="${user.id}"]`))
.toContainText('更新的用户名')
// 删除用户
const deleteResponse = await request.delete(`/api/users/${user.id}`)
expect(deleteResponse.ok()).toBeTruthy()
// 验证用户已删除
await page.reload()
await expect(page.locator(`[data-user-id="${user.id}"]`)).toHaveCount(0)
})
test('文件上传功能', async ({ page }) => {
await page.goto('/upload')
// 准备测试文件
const fileChooserPromise = page.waitForEvent('filechooser')
await page.click('[data-testid="file-upload-button"]')
const fileChooser = await fileChooserPromise
await fileChooser.setFiles({
name: 'test.txt',
mimeType: 'text/plain',
buffer: Buffer.from('这是测试文件内容')
})
// 验证文件上传
await expect(page.locator('[data-testid="file-name"]')).toContainText('test.txt')
// 提交上传
await page.click('[data-testid="upload-submit"]')
// 验证上传成功
await expect(page.locator('[data-testid="upload-success"]'))
.toContainText('文件上传成功')
})
})# 🎉 .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
cypress-tests:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, edge]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Start application
run: npm run preview &
- name: Wait for application
run: npx wait-on http://localhost:4173
- name: Run Cypress tests
uses: cypress-io/github-action@v5
with:
browser: ${{ matrix.browser }}
record: true
parallel: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-videos-${{ matrix.browser }}
path: cypress/videos
playwright-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Build application
run: npm run build
- name: Run Playwright tests
run: npx playwright test
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30# 🎉 Dockerfile.e2e - E2E测试Docker环境
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
# 复制依赖文件
COPY package*.json ./
# 安装依赖
RUN npm ci
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 安装浏览器
RUN npx playwright install
# 运行测试
CMD ["npm", "run", "test:e2e"]// 🎉 cypress/support/pages/LoginPage.js
class LoginPage {
constructor() {
this.emailInput = '[data-testid="email-input"]'
this.passwordInput = '[data-testid="password-input"]'
this.loginButton = '[data-testid="login-button"]'
this.errorMessage = '[data-testid="error-message"]'
}
visit() {
cy.visit('/login')
return this
}
fillEmail(email) {
cy.get(this.emailInput).type(email)
return this
}
fillPassword(password) {
cy.get(this.passwordInput).type(password)
return this
}
submit() {
cy.get(this.loginButton).click()
return this
}
login(email, password) {
return this.fillEmail(email)
.fillPassword(password)
.submit()
}
shouldShowError(message) {
cy.get(this.errorMessage).should('contain', message)
return this
}
}
export default LoginPage
// 使用页面对象
import LoginPage from '../support/pages/LoginPage'
describe('登录功能测试', () => {
const loginPage = new LoginPage()
it('应该能够成功登录', () => {
loginPage
.visit()
.login('test@example.com', 'password123')
cy.url().should('include', '/dashboard')
})
})// 🎉 cypress/support/commands.js
// 登录命令
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.visit('/login')
cy.get('[data-testid="email-input"]').type(email)
cy.get('[data-testid="password-input"]').type(password)
cy.get('[data-testid="login-button"]').click()
cy.url().should('include', '/dashboard')
})
})
// 数据库种子命令
Cypress.Commands.add('seedDatabase', () => {
cy.task('seedDatabase')
})
// 等待加载命令
Cypress.Commands.add('waitForPageLoad', () => {
cy.get('[data-testid="loading"]').should('not.exist')
cy.get('[data-testid="content"]').should('be.visible')
})
// 截图命令
Cypress.Commands.add('takeScreenshot', (name) => {
cy.screenshot(name, { capture: 'fullPage' })
})
// 类型声明
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>
seedDatabase(): Chainable<void>
waitForPageLoad(): Chainable<void>
takeScreenshot(name: string): Chainable<void>
}
}
}通过本节Vue3 E2E测试的学习,你已经掌握:
A: E2E测试在真实浏览器环境中测试完整用户流程,包括前端、后端、数据库等所有组件;集成测试主要验证组件间的接口和协作,通常不涉及UI层面。
A: Cypress更适合快速开始和简单场景,有优秀的开发体验;Playwright支持更多浏览器,性能更好,适合复杂的跨浏览器测试需求。
A: 可以通过并行执行、选择性运行、优化测试用例、使用无头浏览器等方式提升执行效率。关键是平衡覆盖率和执行时间。
A: 使用显式等待而非固定延时、合理设置超时时间、隔离测试数据、重试机制、详细的错误日志等方法提升测试稳定性。
A: 建议覆盖核心用户路径和关键业务流程,通常占总功能的20-30%即可。重点关注高价值、高风险的功能点。
// 问题:元素查找失败
// 解决:使用显式等待
// ❌ 错误写法
cy.get('[data-testid="button"]').click()
// ✅ 正确写法
cy.get('[data-testid="button"]', { timeout: 10000 })
.should('be.visible')
.and('not.be.disabled')
.click()// 问题:异步加载内容测试失败
// 解决:等待特定状态
// ❌ 错误写法
cy.visit('/page')
cy.get('[data-testid="content"]').should('contain', 'data')
// ✅ 正确写法
cy.visit('/page')
cy.get('[data-testid="loading"]').should('not.exist')
cy.get('[data-testid="content"]').should('be.visible')
cy.get('[data-testid="content"]').should('contain', 'data')"掌握Vue3 E2E测试,为你的应用质量提供最后的保障。E2E测试是用户体验和产品质量的守护者!"