Vue 端到端测试:Cypress/Vitest测试流程
【摘要】 一、引言1.1 端到端测试的重要性端到端测试(E2E Testing)是现代前端开发的质量保证基石,通过模拟真实用户行为,验证整个应用流程的正确性。在Vue.js应用开发中,Cypress和Vitest提供了强大的E2E测试能力,确保用户体验和业务功能的高质量交付。1.2 技术价值与市场分析class E2ETestingAnalysis { /** E2E测试市场分析 */ s...
一、引言
1.1 端到端测试的重要性
1.2 技术价值与市场分析
class E2ETestingAnalysis {
/** E2E测试市场分析 */
static getMarketAnalysis() {
return {
'测试覆盖率': '企业级项目E2E测试覆盖率达70-85%',
'缺陷发现': 'E2E测试发现15-20%的UI和集成缺陷',
'回归效率': '自动化E2E测试比手动测试快10-20倍',
'质量成本': '自动化测试降低质量成本40-60%',
'发布信心': '完整E2E测试套件提升发布信心90%+'
};
}
/** 测试方案对比 */
static getTechnologyComparison() {
return {
'Cypress vs Selenium': {
'执行速度': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐',
'调试体验': '⭐⭐⭐⭐⭐ vs ⭐⭐',
'配置复杂度': '⭐⭐⭐⭐⭐ vs ⭐⭐',
'稳定性': '⭐⭐⭐⭐ vs ⭐⭐⭐',
'社区生态': '⭐⭐⭐⭐ vs ⭐⭐⭐⭐⭐'
},
'Vitest vs Jest': {
'启动速度': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐',
'HMR支持': '⭐⭐⭐⭐⭐ vs ⭐',
'Vue集成': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐',
'类型安全': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐⭐',
'兼容性': '⭐⭐⭐ vs ⭐⭐⭐⭐⭐'
},
'Cypress vs Playwright': {
'录制功能': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐⭐',
'时间旅行': '⭐⭐⭐⭐⭐ vs ⭐⭐⭐',
'跨浏览器': '⭐⭐⭐ vs ⭐⭐⭐⭐⭐',
'移动端': '⭐⭐⭐ vs ⭐⭐⭐⭐',
'API测试': '⭐⭐⭐⭐ vs ⭐⭐⭐⭐⭐'
}
};
}
/** 业务价值分析 */
static getBusinessValue() {
return {
'质量保证': '端到端验证确保业务逻辑正确性',
'回归安全': '自动化回归防止功能回退',
'开发效率': '快速反馈加速开发迭代',
'用户体验': '真实用户场景验证提升满意度',
'团队协作': '活文档促进团队理解和协作'
};
}
}
1.3 测试效率与质量基准
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
二、技术背景
2.1 Vue E2E测试架构
graph TB
A[Vue E2E测试架构] --> B[测试框架层]
A --> C[测试工具层]
A --> D[测试服务层]
A --> E[CI/CD集成层]
B --> B1[Cypress]
B --> B2[Vitest]
B --> B3[Playwright]
B --> B4[WebdriverIO]
C --> C1[测试运行器]
C --> C2[断言库]
C --> C3[模拟工具]
C --> C4[报告生成]
D --> D1[测试数据管理]
D --> D2[环境配置]
D --> D3[快照管理]
D --> D4[性能监控]
E --> E1[GitHub Actions]
E --> E2[Jenkins]
E --> E3[GitLab CI]
E --> E4[Docker]
B1 --> F[测试执行]
C1 --> F
D1 --> F
F --> G[质量报告]
2.2 核心技术栈
// 测试技术栈配置
export const E2ETestingTechStack = {
// 测试框架
cypress: {
version: '^12.0.0',
features: [
'实时重载',
'时间旅行调试',
'自动等待',
'网络控制',
'可视化测试'
]
},
vitest: {
version: '^0.34.0',
features: [
'Vite原生支持',
'超快执行速度',
'ESM原生',
'组件测试',
'快照测试'
]
},
// 测试类型
testingTypes: {
e2e: ['用户流程', '业务场景', '集成测试'],
component: ['组件交互', 'Props验证', '事件测试'],
api: ['接口测试', '数据流', '错误处理'],
visual: ['UI回归', '视觉测试', '响应式测试']
},
// 测试工具
utilities: {
mocking: ['API模拟', '数据库模拟', '文件系统模拟'],
fixtures: ['测试数据', '环境配置', '用户会话'],
reporters: ['HTML报告', 'JUnit报告', '自定义报告'],
plugins: ['代码覆盖率', '性能监控', '无障碍测试']
}
};
三、环境准备与配置
3.1 项目依赖配置
// package.json
{
"name": "vue-e2e-testing",
"version": "1.0.0",
"type": "module",
"scripts": {
"test:e2e": "cypress run",
"test:e2e:open": "cypress open",
"test:e2e:headed": "cypress run --headed",
"test:component": "vitest",
"test:component:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:all": "npm run test:component && npm run test:e2e"
},
"devDependencies": {
"cypress": "^12.0.0",
"vitest": "^0.34.0",
"@vue/test-utils": "^2.4.0",
"@vitest/ui": "^0.34.0",
"@vitest/coverage-v8": "^0.34.0",
"jsdom": "^22.0.0",
"happy-dom": "^8.0.0"
}
}
3.2 Cypress配置文件
// cypress.config.js
import { defineConfig } from 'cypress'
export default defineConfig({
// 项目配置
projectId: 'your-project-id',
// 视口设置
viewportWidth: 1920,
viewportHeight: 1080,
// 超时设置
defaultCommandTimeout: 10000,
responseTimeout: 30000,
requestTimeout: 5000,
// 重试次数
retries: {
runMode: 2,
openMode: 0
},
// 环境变量
env: {
apiUrl: 'http://localhost:3000/api',
authToken: process.env.CYPRESS_AUTH_TOKEN
},
// e2e测试配置
e2e: {
baseUrl: 'http://localhost:5173',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/e2e.js',
setupNodeEvents(on, config) {
// 插件配置
require('@cypress/grep/src/plugin')(config)
// 自定义任务
on('task', {
log(message) {
console.log(message)
return null
},
// 数据库操作
async queryDb(query) {
return await runDatabaseQuery(query)
}
})
return config
}
},
// 组件测试配置
component: {
devServer: {
framework: 'vue',
bundler: 'vite'
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}'
}
})
3.3 Vitest配置文件
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
// 测试环境
environment: 'happy-dom',
// 全局设置
globals: true,
// 覆盖率配置
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: [
'coverage/**',
'dist/**',
'**/[.]**',
'packages/*/test?(s)/**',
'**/*.d.ts',
'**/virtual:*',
'**/__x00__**',
'**/\x00*',
'cypress/**',
'test?(s)/**',
'test?(-*).?(c|m)[jt]s?(x)',
'**/*{.,-}test.?(c|m)[jt]s?(x)',
'**/*{.,-}spec.?(c|m)[jt]s?(x)',
'**/__tests__/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*'
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
},
// 测试文件匹配
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
// 别名配置
alias: {
'@': new URL('./src', import.meta.url).pathname
},
// 超时设置
testTimeout: 10000,
hookTimeout: 30000
}
})
四、核心测试实现
4.1 Cypress E2E测试实现
// cypress/e2e/user-auth.cy.js
describe('用户认证流程', () => {
beforeEach(() => {
// 每次测试前访问首页
cy.visit('/')
})
describe('登录功能', () => {
it('应该成功登录并跳转到仪表板', () => {
// 1. 访问登录页面
cy.visit('/login')
// 2. 填写登录表单
cy.get('[data-testid="email-input"]')
.type('user@example.com')
.should('have.value', 'user@example.com')
cy.get('[data-testid="password-input"]')
.type('password123')
.should('have.value', 'password123')
// 3. 提交表单
cy.get('[data-testid="login-button"]')
.click()
// 4. 验证跳转和用户状态
cy.url().should('include', '/dashboard')
cy.get('[data-testid="user-avatar"]')
.should('be.visible')
cy.get('[data-testid="welcome-message"]')
.should('contain', '欢迎回来')
// 5. 验证本地存储
cy.window().its('localStorage.authToken')
.should('exist')
})
it('应该显示登录错误信息', () => {
// 模拟API错误
cy.intercept('POST', '/api/auth/login', {
statusCode: 401,
body: { message: '无效的凭据' }
}).as('loginRequest')
cy.visit('/login')
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()
// 等待API调用
cy.wait('@loginRequest')
// 验证错误消息
cy.get('[data-testid="error-message"]')
.should('be.visible')
.and('contain', '无效的凭据')
})
it('应该验证表单输入', () => {
cy.visit('/login')
// 测试空提交
cy.get('[data-testid="login-button"]').click()
cy.get('[data-testid="email-error"]')
.should('be.visible')
.and('contain', '邮箱是必需的')
cy.get('[data-testid="password-error"]')
.should('be.visible')
.and('contain', '密码是必需的')
// 测试无效邮箱格式
cy.get('[data-testid="email-input"]')
.type('invalid-email')
.blur()
cy.get('[data-testid="email-error"]')
.should('contain', '请输入有效的邮箱地址')
})
})
describe('注册功能', () => {
it('应该成功注册新用户', () => {
cy.visit('/register')
// 生成测试数据
const testUser = {
name: '测试用户',
email: `test${Date.now()}@example.com`,
password: 'Password123!'
}
// 填写注册表单
cy.get('[data-testid="name-input"]').type(testUser.name)
cy.get('[data-testid="email-input"]').type(testUser.email)
cy.get('[data-testid="password-input"]').type(testUser.password)
cy.get('[data-testid="confirm-password-input"]').type(testUser.password)
// 同意条款
cy.get('[data-testid="terms-checkbox"]').check()
// 提交注册
cy.get('[data-testid="register-button"]').click()
// 验证成功注册
cy.url().should('include', '/welcome')
cy.get('[data-testid="success-message"]')
.should('contain', '注册成功')
})
})
describe('密码重置', () => {
it('应该发送密码重置邮件', () => {
cy.visit('/forgot-password')
cy.get('[data-testid="email-input"]')
.type('user@example.com')
cy.intercept('POST', '/api/auth/forgot-password', {
statusCode: 200,
body: { message: '重置邮件已发送' }
}).as('forgotPassword')
cy.get('[data-testid="submit-button"]').click()
cy.wait('@forgotPassword')
cy.get('[data-testid="success-message"]')
.should('contain', '重置邮件已发送')
})
})
})
4.2 电商流程E2E测试
// cypress/e2e/ecommerce.cy.js
describe('电商购物流程', () => {
beforeEach(() => {
// 登录用户
cy.login('customer@example.com', 'password123')
cy.visit('/products')
})
describe('商品浏览和搜索', () => {
it('应该能够浏览商品列表', () => {
cy.get('[data-testid="product-card"]')
.should('have.length.at.least', 1)
// 验证商品信息
cy.get('[data-testid="product-card"]')
.first()
.within(() => {
cy.get('[data-testid="product-name"]').should('be.visible')
cy.get('[data-testid="product-price"]').should('be.visible')
cy.get('[data-testid="add-to-cart"]').should('be.visible')
})
})
it('应该能够搜索商品', () => {
const searchTerm = '笔记本电脑'
cy.get('[data-testid="search-input"]')
.type(searchTerm)
.type('{enter}')
cy.url().should('include', `search=${encodeURIComponent(searchTerm)}`)
// 验证搜索结果
cy.get('[data-testid="product-card"]')
.each(($product) => {
cy.wrap($product)
.find('[data-testid="product-name"]')
.invoke('text')
.should('match', new RegExp(searchTerm, 'i'))
})
})
it('应该能够过滤商品', () => {
// 按价格过滤
cy.get('[data-testid="price-filter"]').select('1000-2000')
cy.get('[data-testid="product-card"]')
.each(($product) => {
cy.wrap($product)
.find('[data-testid="product-price"]')
.invoke('text')
.then(priceText => {
const price = parseFloat(priceText.replace(/[^0-9.]/g, ''))
expect(price).to.be.within(1000, 2000)
})
})
})
})
describe('购物车功能', () => {
it('应该能够添加商品到购物车', () => {
cy.get('[data-testid="product-card"]')
.first()
.within(() => {
cy.get('[data-testid="add-to-cart"]').click()
})
// 验证购物车数量更新
cy.get('[data-testid="cart-count"]')
.should('contain', '1')
// 验证成功提示
cy.get('[data-testid="toast-message"]')
.should('contain', '已添加到购物车')
})
it('应该能够查看购物车', () => {
// 先添加商品
cy.addProductToCart(1)
cy.get('[data-testid="cart-icon"]').click()
cy.url().should('include', '/cart')
// 验证购物车内容
cy.get('[data-testid="cart-item"]')
.should('have.length', 1)
.within(() => {
cy.get('[data-testid="product-name"]').should('be.visible')
cy.get('[data-testid="quantity-input"]').should('have.value', '1')
cy.get('[data-testid="item-total"]').should('be.visible')
})
})
it('应该能够更新商品数量', () => {
cy.addProductToCart(1)
cy.visit('/cart')
cy.get('[data-testid="quantity-input"]')
.clear()
.type('3')
.blur()
// 验证总价更新
cy.get('[data-testid="cart-total"]')
.invoke('text')
.then(totalText => {
const total = parseFloat(totalText.replace(/[^0-9.]/g, ''))
cy.get('[data-testid="item-price"]')
.invoke('text')
.then(priceText => {
const price = parseFloat(priceText.replace(/[^0-9.]/g, ''))
expect(total).to.equal(price * 3)
})
})
})
it('应该能够删除购物车商品', () => {
cy.addProductToCart(1)
cy.visit('/cart')
cy.get('[data-testid="remove-item"]').click()
cy.get('[data-testid="empty-cart-message"]')
.should('be.visible')
.and('contain', '购物车为空')
})
})
describe('结算流程', () => {
it('应该完成完整的购买流程', () => {
// 添加商品到购物车
cy.addProductToCart(1)
// 进入结算页面
cy.get('[data-testid="checkout-button"]').click()
cy.url().should('include', '/checkout')
// 填写配送信息
cy.fillShippingInfo({
name: '测试用户',
address: '测试地址 123号',
city: '测试市',
zipCode: '100000',
phone: '13800138000'
})
// 选择支付方式
cy.get('[data-testid="payment-method"]').select('credit_card')
// 填写支付信息
cy.fillPaymentInfo({
cardNumber: '4111111111111111',
expiry: '12/25',
cvv: '123'
})
// 提交订单
cy.intercept('POST', '/api/orders', {
statusCode: 201,
body: { orderId: 'ORD-123456', status: 'confirmed' }
}).as('createOrder')
cy.get('[data-testid="place-order"]').click()
cy.wait('@createOrder')
// 验证订单确认页面
cy.url().should('include', '/order-confirmation')
cy.get('[data-testid="order-success"]')
.should('contain', '订单提交成功')
cy.get('[data-testid="order-number"]')
.should('contain', 'ORD-123456')
})
})
})
4.3 Vitest组件测试实现
// src/components/__tests__/ProductCard.spec.js
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import ProductCard from '../ProductCard.vue'
// 模拟数据
const mockProduct = {
id: 1,
name: '测试商品',
price: 99.99,
image: 'test-image.jpg',
description: '这是一个测试商品',
inStock: true,
rating: 4.5
}
// 模拟函数
const mockAddToCart = vi.fn()
describe('ProductCard 组件', () => {
const createWrapper = (props = {}) => {
return mount(ProductCard, {
props: {
product: mockProduct,
...props
},
global: {
mocks: {
$store: {
dispatch: mockAddToCart
}
},
stubs: {
'router-link': true
}
}
})
}
describe('渲染测试', () => {
it('应该正确渲染商品信息', () => {
const wrapper = createWrapper()
expect(wrapper.find('[data-testid="product-name"]').text())
.toBe(mockProduct.name)
expect(wrapper.find('[data-testid="product-price"]').text())
.toContain('99.99')
expect(wrapper.find('[data-testid="product-image"]').attributes('src'))
.toBe(mockProduct.image)
expect(wrapper.find('[data-testid="product-rating"]').text())
.toContain('4.5')
})
it('应该显示库存状态', () => {
const wrapper = createWrapper()
expect(wrapper.find('[data-testid="in-stock"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="out-of-stock"]').exists()).toBe(false)
})
it('应该显示缺货状态当商品无库存时', () => {
const outOfStockProduct = { ...mockProduct, inStock: false }
const wrapper = createWrapper({ product: outOfStockProduct })
expect(wrapper.find('[data-testid="out-of-stock"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="add-to-cart"]').attributes('disabled'))
.toBeDefined()
})
})
describe('交互测试', () => {
it('点击加入购物车应该触发事件', async () => {
const wrapper = createWrapper()
await wrapper.find('[data-testid="add-to-cart"]').trigger('click')
expect(mockAddToCart).toHaveBeenCalledWith('cart/addItem', {
product: mockProduct,
quantity: 1
})
})
it('点击商品应该跳转到详情页', async () => {
const wrapper = createWrapper()
const routerPush = vi.fn()
wrapper.vm.$router = { push: routerPush }
await wrapper.find('[data-testid="product-link"]').trigger('click')
expect(routerPush).toHaveBeenCalledWith({
name: 'ProductDetail',
params: { id: mockProduct.id }
})
})
it('应该处理数量变化', async () => {
const wrapper = createWrapper()
const quantityInput = wrapper.find('[data-testid="quantity-input"]')
await quantityInput.setValue(3)
expect(quantityInput.element.value).toBe('3')
})
})
describe('样式和外观', () => {
it('应该应用正确的CSS类', () => {
const wrapper = createWrapper()
expect(wrapper.find('.product-card').exists()).toBe(true)
expect(wrapper.find('.product-card--featured').exists()).toBe(false)
})
it('特色商品应该有特殊样式', () => {
const featuredProduct = { ...mockProduct, isFeatured: true }
const wrapper = createWrapper({ product: featuredProduct })
expect(wrapper.find('.product-card--featured').exists()).toBe(true)
})
it('应该响应式布局', async () => {
const wrapper = createWrapper()
// 测试移动端布局
Object.defineProperty(window, 'innerWidth', {
writable: true,
configurable: true,
value: 375
})
window.dispatchEvent(new Event('resize'))
await wrapper.vm.$nextTick()
expect(wrapper.find('.product-card--mobile').exists()).toBe(true)
})
})
})
4.4 API测试和模拟
// cypress/e2e/api-tests.cy.js
describe('API 测试', () => {
describe('用户认证API', () => {
it('应该成功登录并返回token', () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
email: 'user@example.com',
password: 'password123'
}
}).then((response) => {
expect(response.status).to.equal(200)
expect(response.body).to.have.property('token')
expect(response.body.user).to.have.property('email', 'user@example.com')
})
})
it('应该拒绝无效凭据', () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
email: 'wrong@example.com',
password: 'wrongpassword'
},
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.equal(401)
expect(response.body).to.have.property('message', '无效的凭据')
})
})
})
describe('商品API', () => {
it('应该获取商品列表', () => {
cy.request('/api/products')
.then((response) => {
expect(response.status).to.equal(200)
expect(response.body).to.be.an('array')
expect(response.body[0]).to.have.keys([
'id', 'name', 'price', 'description', 'image'
])
})
})
it('应该支持分页', () => {
cy.request('/api/products?page=1&limit=10')
.then((response) => {
expect(response.status).to.equal(200)
expect(response.body.data).to.have.length.of.at.most(10)
expect(response.body).to.have.property('total')
expect(response.body).to.have.property('page', 1)
})
})
})
})
// API模拟示例
describe('使用模拟API的测试', () => {
beforeEach(() => {
// 模拟API响应
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: {
data: [
{
id: 1,
name: '模拟商品1',
price: 100,
image: 'image1.jpg'
},
{
id: 2,
name: '模拟商品2',
price: 200,
image: 'image2.jpg'
}
],
total: 2,
page: 1
}
}).as('getProducts')
})
it('应该显示模拟的商品数据', () => {
cy.visit('/products')
cy.wait('@getProducts')
cy.get('[data-testid="product-card"]').should('have.length', 2)
cy.contains('模拟商品1').should('be.visible')
cy.contains('模拟商品2').should('be.visible')
})
})
五、高级测试技巧
5.1 自定义命令和工具函数
// cypress/support/commands.js
// 自定义登录命令
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token)
})
})
// 访问首页确保登录状态
cy.visit('/')
})
// 自定义添加商品到购物车命令
Cypress.Commands.add('addProductToCart', (productId, quantity = 1) => {
cy.request({
method: 'POST',
url: '/api/cart/items',
body: {
productId,
quantity
},
headers: {
Authorization: `Bearer ${window.localStorage.getItem('authToken')}`
}
})
})
// 自定义表单填写命令
Cypress.Commands.add('fillShippingInfo', (info) => {
cy.get('[data-testid="name-input"]').type(info.name)
cy.get('[data-testid="address-input"]').type(info.address)
cy.get('[data-testid="city-input"]').type(info.city)
cy.get('[data-testid="zipcode-input"]').type(info.zipCode)
cy.get('[data-testid="phone-input"]').type(info.phone)
})
// 自定义断言命令
Cypress.Commands.add('assertRedirect', (path) => {
cy.url().should('include', path)
})
// 工具函数
export const generateTestData = {
user: (overrides = {}) => ({
name: '测试用户',
email: `test${Date.now()}@example.com`,
password: 'Password123!',
...overrides
}),
product: (overrides = {}) => ({
name: `测试商品${Date.now()}`,
price: Math.floor(Math.random() * 1000) + 100,
description: '测试商品描述',
...overrides
}),
order: (overrides = {}) => ({
items: [
{ productId: 1, quantity: 2 },
{ productId: 2, quantity: 1 }
],
shippingAddress: '测试地址',
...overrides
})
}
5.2 测试配置和环境管理
// cypress/config/environments.js
export const environments = {
development: {
baseUrl: 'http://localhost:5173',
apiUrl: 'http://localhost:3000/api',
adminEmail: 'admin@dev.com',
adminPassword: 'dev123'
},
staging: {
baseUrl: 'https://staging.example.com',
apiUrl: 'https://api.staging.example.com',
adminEmail: process.env.STAGING_ADMIN_EMAIL,
adminPassword: process.env.STAGING_ADMIN_PASSWORD
},
production: {
baseUrl: 'https://example.com',
apiUrl: 'https://api.example.com',
adminEmail: process.env.PROD_ADMIN_EMAIL,
adminPassword: process.env.PROD_ADMIN_PASSWORD
}
}
// 根据环境获取配置
export const getConfig = () => {
const environment = Cypress.env('environment') || 'development'
return environments[environment]
}
六、CI/CD集成
6.1 GitHub Actions配置
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
e2e-tests:
runs-on: ubuntu-latest
services:
# 启动测试数据库
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
strategy:
matrix:
node-version: [16.x, 18.x]
browser: [chrome, firefox]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start development server
run: npm run dev &
- name: Wait for server
run: npx wait-on http://localhost:5173
- name: Run E2E tests
run: npm run test:e2e -- --browser ${{ matrix.browser }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
DATABASE_URL: postgresql://postgres:test@localhost:5432/test_db
- name: Upload test results
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-videos
path: cypress/videos
七、测试报告和监控
7.1 测试报告配置
// cypress/config/reporter.js
import mochawesome from 'cypress-mochawesome-reporter'
module.exports = (on, config) => {
on('after:run', (results) => {
// 生成自定义报告
if (results) {
// 发送到监控系统
sendResultsToMonitoring(results)
// 生成HTML报告
generateHTMLReport(results)
// 发送通知
if (results.totalFailed > 0) {
sendFailureNotification(results)
}
}
})
}
// 自定义报告生成
const generateHTMLReport = (results) => {
const report = {
summary: {
total: results.totalTests,
passed: results.totalPassed,
failed: results.totalFailed,
duration: results.totalDuration
},
browser: results.browserName,
version: results.browserVersion,
specs: results.runs.map(run => ({
file: run.spec.name,
tests: run.tests,
error: run.error
}))
}
// 保存报告文件
fs.writeFileSync('cypress/reports/results.json', JSON.stringify(report, null, 2))
}
八、总结
8.1 技术成果总结
核心测试能力
- •
完整的用户流程测试:登录、浏览、购物、支付等端到端验证 - •
组件级测试:Vue组件交互、Props、事件的全方位测试 - •
API测试:接口验证、错误处理、数据流测试 - •
视觉回归测试:UI一致性、响应式布局验证
测试质量指标
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8.2 开发效率提升
测试效率对比
class TestingEfficiency {
static getEfficiencyComparison() {
return {
'手动测试': {
'执行时间': '2-8小时/次',
'反馈周期': '1-2天',
'覆盖范围': '有限场景',
'维护成本': '高',
'可靠性': '中等'
},
'自动化E2E测试': {
'执行时间': '5-15分钟/次',
'反馈周期': '实时',
'覆盖范围': '完整流程',
'维护成本': '中',
'可靠性': '高'
}
}
}
static getROIAnalysis() {
return {
'初期投入': '2-4周设置时间',
'长期收益': '测试效率提升10倍',
'质量提升': '缺陷减少60-80%',
'发布信心': '从70%提升至95%+',
'团队效率': '开发速度提升25-40%'
}
}
}
8.3 最佳实践总结
测试策略最佳实践
class E2EBestPractices {
static getTestingStrategy() {
return {
'测试金字塔': {
'单元测试': '70% - 快速反馈,基础保障',
'集成测试': '20% - 模块交互,接口验证',
'E2E测试': '10% - 关键流程,用户体验'
},
'测试数据管理': {
'工厂模式': '可复用的测试数据生成',
'固定数据': '已知输入的预期输出',
'清理策略': '每个测试后重置状态'
},
'测试执行策略': {
'并行执行': '利用多核加速测试',
'智能重试': '处理flaky tests',
'失败优先': '优先修复失败测试'
}
}
}
static getCodeQualityPractices() {
return {
'可读性': '描述性测试名称,清晰的结构',
'可维护性': '页面对象模式,自定义命令',
'可靠性': '智能等待,稳定的选择器',
'可扩展性': '模块化设计,配置化驱动'
}
}
}
8.4 未来展望
测试技术趋势
class E2EFutureTrends {
static getTechnologyTrends() {
return {
'2024': [
'AI生成测试用例',
'智能测试修复',
'可视化测试编排',
'跨平台测试统一'
],
'2025': [
'自愈性测试',
'预测性测试分析',
'元宇宙环境测试',
'量子测试加速'
]
}
}
static getDevelopmentTrends() {
return {
'低代码测试': '可视化测试创建和维护',
'智能分析': 'AI驱动的测试优化建议',
'云测试平台': '按需测试基础设施',
'实时协作': '团队实时测试协作'
}
}
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)