泰安手机网站建设电话wordpress分类页面的地址
2026/2/12 12:01:23 网站建设 项目流程
泰安手机网站建设电话,wordpress分类页面的地址,wordpress 路由设计,联盟营销是一种 的网络营销方式1. 那个让我加班到凌晨两点的测试场景去年我们团队接到一个紧急需求#xff1a;测试一个预约挂号系统。一切都挺顺利#xff0c;直到遇到这个场景——“当号源被抢光时#xff0c;显示候补排队功能”。问题来了#xff1a;我们怎么在自动化测试里模拟“号源瞬间被抢光”的状…1. 那个让我加班到凌晨两点的测试场景去年我们团队接到一个紧急需求测试一个预约挂号系统。一切都挺顺利直到遇到这个场景——“当号源被抢光时显示候补排队功能”。问题来了我们怎么在自动化测试里模拟“号源瞬间被抢光”的状态最初我们尝试了各种歪门邪道手动修改数据库、写脚本清空号源、甚至想用两个测试账号同时操作……直到周五晚上11点第6次尝试失败后我盯着控制台里那些真实的HTTP请求突然意识到我们一直在解决错误的问题。真正的解决方案不是去操纵真实系统状态而是拦截请求直接返回我们想要的响应。这就是Mock Service WorkerMSW进入我们技术栈的开始。2. 为什么传统的Mock方法让我们痛苦不堪先看看我们曾经尝试过的几种方案方案A直接修改业务代码// ❌ 测试代码侵入业务逻辑 if (process.env.NODE_ENV test) { mockData require(./test-data.json); return res.json(mockData); } // 生产环境代码...方案B在测试中覆写fetch// ❌ 混乱不堪难以维护 beforeEach(() { window.fetch jest.fn().mockImplementation(() { return Promise.resolve({ json: () Promise.resolve({ tickets: 0 }) }); }); });方案C搭建一个假的测试服务器# ❌ 开发、维护成本太高 $ npm run start-mock-server $ npm run start-test-server $ npm run start-dev-server # 到底该启动哪个这些方案要么污染生产代码要么难以维护要么需要复杂的本地环境。直到我们发现MSW才真正解决了这些问题。3. MSW的核心优势像真实服务器一样工作Mock Service WorkerMSW是一个基于Service Worker的API mocking库。它的工作原理很巧妙在浏览器中注册Service Worker拦截所有网络请求匹配请求模式决定是否要拦截返回模拟的响应而不是发送到真实服务器// 这是MSW的基本工作原理示意图 // [你的应用] -- [fetch(/api/tickets)] // ↓ // [Service Worker 拦截] // ↓ // [匹配路由 handlers] // ↓ // [返回模拟响应 {tickets: 0}] // ↓ // [应用收到响应]关键优势在于你的应用完全不知道自己在被mock。它发送真实的HTTP请求收到真实的HTTP响应只是中间的过程被我们“偷梁换柱”了。4. 一步步搭建Playwright MSW环境4.1 安装必要的包# 安装MSW核心库 npm install msw --save-dev # Playwright测试工具 npm install playwright/test --save-dev # 类型定义TypeScript项目需要 npm install types/msw --save-dev4.2 创建模拟处理器mocks/handlers.js// 模拟处理器 - 定义各种API的mock响应 import { http, HttpResponse } frommsw; exportconst handlers [ // 1. 模拟获取号源列表 http.get(/api/tickets, ({ request }) { const url new URL(request.url); const date url.searchParams.get(date); const department url.searchParams.get(department); console.log([MSW] 拦截请求: /api/tickets?date${date}department${department}); // 根据日期和科室返回不同数据 if (date 2024-06-15 department cardiovascular) { // 模拟心内科号源已抢光 return HttpResponse.json({ success: true, data: { available: false, tickets: 0, waitingCount: 42, nextAvailableDate: 2024-06-20 }, message: 号源已满可加入候补 }); } // 默认返回有号源的情况 return HttpResponse.json({ success: true, data: { available: true, tickets: 12, waitingCount: 0, nextAvailableDate: null } }); }), // 2. 模拟提交预约 http.post(/api/appointments, async ({ request }) { const body await request.json(); console.log([MSW] 创建预约:, body); // 模拟10%的失败率测试异常流程 const shouldFail Math.random() 0.1; if (shouldFail) { return HttpResponse.json( { success: false, error: SYSTEM_BUSY, message: 系统繁忙请稍后重试 }, { status: 503 } ); } // 成功响应 return HttpResponse.json({ success: true, data: { appointmentId: APT${Date.now()}, status: PENDING, queuePosition: body.waitList ? 15 : null, estimatedTime: body.waitList ? 2-3工作日 : 立即确认 } }); }), // 3. 模拟取消预约 http.delete(/api/appointments/:id, ({ params }) { const { id } params; // 模拟特定的预约ID不能取消 if (id APT_NO_CANCEL) { return HttpResponse.json( { success: false, error: CANCELLATION_NOT_ALLOWED, message: 该预约已过取消截止时间 }, { status: 400 } ); } return HttpResponse.json({ success: true, message: 预约已取消 }); }), // 4. 模拟GraphQL请求如果项目使用 http.post(/graphql, async ({ request }) { const { query, variables } await request.json(); if (query.includes(GetPatientInfo)) { return HttpResponse.json({ data: { patient: { id: variables.id, name: 测试用户, idCard: 110101199001011234, phone: 13800138000 } } }); } return HttpResponse.json({ data: {} }); }), // 5. 模拟文件上传 http.post(/api/upload, async () { // 模拟上传进度 awaitnewPromise(resolve setTimeout(resolve, 500)); return HttpResponse.json({ success: true, url: https://mock-cdn.com/uploads/test-image.jpg, size: 204800, filename: test-upload.jpg }); }) ];4.3 配置Service Workermocks/browser.js// 浏览器环境使用的MSW设置 import { setupWorker } frommsw/browser; import { handlers } from./handlers; // 创建worker实例 exportconst worker setupWorker(...handlers); // 开发工具在控制台暴露一些工具函数 if (typeofwindow ! undefined) { window.__MSW { // 动态修改mock响应 overrideHandler: (method, path, newResponse) { // 这里可以实现动态修改handlers的逻辑 console.log([MSW Debug] 覆盖 ${method} ${path}); }, // 查看当前拦截的请求 getRequestLog: () { returnwindow.__mswRequests || []; }, // 模拟网络错误 simulateNetworkError: (shouldFail true) { window.__mswNetworkError shouldFail; } }; }4.4 为Playwright创建专用配置tests/msw-setup.js// Playwright专用的MSW配置 import { createServer } fromhttp; import { setupServer } frommsw/node; import { handlers } from../mocks/handlers; // 创建Node.js环境下的mock server exportconst server setupServer(...handlers); // 扩展handlers添加一些测试专用的mock exportconst testHandlers { // 强制让某个接口失败 forceFail: (method, url) { server.use( http[method.toLowerCase()](url, () { returnnew Response(null, { status: 500 }); }) ); }, // 延迟响应测试loading状态 delayResponse: (method, url, delayMs) { server.use( http[method.toLowerCase()](url, async () { awaitnewPromise(resolve setTimeout(resolve, delayMs)); return HttpResponse.json({ delayed: true }); }) ); }, // 验证请求参数 captureRequests: (method, url) { const requests []; server.use( http[method.toLowerCase()](url, async ({ request }) { const body await request.text(); requests.push({ url: request.url, method: request.method, body: body ? JSON.parse(body) : null, headers: Object.fromEntries(request.headers.entries()), timestamp: newDate().toISOString() }); return HttpResponse.json({ captured: true }); }) ); return requests; } }; // 启动和停止server的实用函数 exportconst startMSW async (page) { // 在页面中注入Service Worker await page.addInitScript(() { // 这里可以注入一些全局的mock配置 window.__TEST_MODE true; window.__MOCK_API true; }); // 启动mock server server.listen({ onUnhandledRequest: (request) { // 对于未处理的请求根据情况决定是否报错 const url request.url.toString(); // 忽略静态资源请求 if (url.includes(.css) || url.includes(.js) || url.includes(.ico)) { return; } // 忽略某些特定的API如果有的话 if (url.includes(/api/health-check)) { return; } // 其他未处理的请求打印警告 console.warn([MSW] 未处理的请求: ${request.method} ${url}); } }); }; exportconst stopMSW () { server.close(); };5. 在Playwright测试中使用MSW5.1 基础测试示例tests/appointment.spec.jsimport { test, expect } fromplaywright/test; import { startMSW, stopMSW, testHandlers } from./msw-setup; // 在每个测试文件开始时启动MSW test.beforeAll(async () { // 这里可以初始化一些全局的mock数据 console.log([Test Setup] 启动MSW Mock Server); }); // 在每个测试用例前设置 test.beforeEach(async ({ page }) { // 启动MSW await startMSW(page); // 跳转到测试页面 await page.goto(/appointment); // 等待必要的元素加载 await page.waitForSelector([data-testidappointment-container]); }); // 测试用例1正常预约流程 test(用户成功预约挂号, async ({ page }) { // 页面已经加载了默认的mock数据有号源 // 1. 选择日期 await page.click([data-testiddate-2024-06-10]); // 2. 选择科室 await page.selectOption([data-testiddepartment-select], internal); // 3. 验证号源显示正确 const ticketCount await page.textContent([data-testidticket-count]); expect(parseInt(ticketCount)).toBeGreaterThan(0); // 4. 选择医生 await page.click([data-testiddoctor-1001]); // 5. 填写患者信息 await page.fill([data-testidpatient-name], 张三); await page.fill([data-testidpatient-id], 110101199001011234); // 6. 提交预约 await page.click([data-testidsubmit-appointment]); // 7. 验证成功提示 await expect(page.locator([data-testidsuccess-message])).toBeVisible(); await expect(page.locator([data-testidappointment-id])).toContainText(APT); }); // 测试用例2号源已抢光的情况 test(当号源被抢光时显示候补排队, async ({ page }) { // 动态修改mock让心内科2024-06-15的号源为0 // 这里我们需要用另一种方式因为MSW在Node环境下运行 // 我们可以通过query参数来触发特定的mock场景 // 1. 直接访问特定日期和科室的组合 await page.goto(/appointment?date2024-06-15departmentcardiovascular); // 2. 验证显示号源已满 await expect(page.locator([data-testidno-tickets-alert])).toBeVisible(); // 3. 验证候补排队按钮显示 await expect(page.locator([data-testidwaitlist-button])).toBeVisible(); // 4. 点击加入候补 await page.click([data-testidwaitlist-button]); // 5. 填写候补信息 await page.fill([data-testidwaitlist-phone], 13800138000); await page.click([data-testidconfirm-waitlist]); // 6. 验证候补成功 await expect(page.locator([data-testidwaitlist-success])).toBeVisible(); const position await page.textContent([data-testidqueue-position]); expect(position).toMatch(/第\d位/); }); // 测试用例3网络异常处理 test(当API请求失败时显示错误信息, async ({ page }) { // 使用testHandlers强制让预约接口失败 // 注意这里需要MSW支持动态修改handlers // 简化方案通过特定参数触发错误 await page.goto(/appointment?forceErrortrue); // 尝试提交预约 await page.fill([data-testidpatient-name], 李四); await page.click([data-testidsubmit-appointment]); // 验证错误提示 await expect(page.locator([data-testiderror-toast])).toBeVisible(); await expect(page.locator([data-testiderror-message])).toContainText(系统繁忙); // 验证重试按钮可用 await expect(page.locator([data-testidretry-button])).toBeEnabled(); }); // 测试用例4取消预约的限制条件 test(处理不能取消的预约, async ({ page }) { // 查看一个特殊的预约不能取消的 await page.goto(/appointment/detail/APT_NO_CANCEL); // 验证取消按钮不可用或有特殊提示 const cancelButton page.locator([data-testidcancel-button]); await expect(cancelButton).toBeDisabled(); // 或者验证有提示信息 await expect(page.locator([data-testidcancel-notice])).toContainText(已过取消时间); }); // 清理 test.afterEach(async () { // 重置MSW handlers避免测试间相互影响 server.resetHandlers(); }); test.afterAll(() { stopMSW(); });5.2 高级用法动态Mock场景tests/msw-dynamic.spec.jsimport { test, expect } fromplaywright/test; import { server } from./msw-setup; import { http, HttpResponse } frommsw; // 动态修改mock响应的测试 test.describe(动态Mock场景, () { let capturedRequests []; test.beforeEach(async ({ page }) { // 清空之前捕获的请求 capturedRequests []; // 动态添加一个handler来捕获请求 server.use( http.post(/api/appointments, async ({ request }) { const body await request.json(); capturedRequests.push({ url: request.url, body, timestamp: newDate().toISOString() }); // 根据不同的测试数据返回不同的响应 if (body.patientName 特殊用户) { return HttpResponse.json({ success: true, special: true, priority: true }); } return HttpResponse.json({ success: true }); }) ); await page.goto(/appointment); }); test(验证请求参数是否正确发送, async ({ page }) { // 填写表单 await page.fill([data-testidpatient-name], 测试用户); await page.fill([data-testidsymptoms], 头痛发热); // 提交 await page.click([data-testidsubmit-appointment]); // 验证捕获的请求 expect(capturedRequests).toHaveLength(1); expect(capturedRequests[0].body).toMatchObject({ patientName: 测试用户, symptoms: 头痛发热 }); }); test(模拟慢速网络, async ({ page }) { // 添加一个延迟响应的handler server.use( http.get(/api/tickets, async () { awaitnewPromise(resolve setTimeout(resolve, 2000)); // 2秒延迟 return HttpResponse.json({ success: true, data: { tickets: 5 } }); }) ); // 验证loading状态显示 await page.click([data-testidrefresh-tickets]); // 应该显示loading await expect(page.locator([data-testidloading-spinner])).toBeVisible(); // 2秒后loading应该消失 await expect(page.locator([data-testidloading-spinner])).not.toBeVisible({ timeout: 3000 }); }); });6. 实际项目中的最佳实践6.1 目录结构建议project/ ├── mocks/ │ ├── handlers/ # 按功能分组的handlers │ │ ├── appointment.js │ │ ├── user.js │ │ └── payment.js │ ├── fixtures/ # mock数据文件 │ │ ├── users.json │ │ └── tickets.json │ ├── utils.js # 工具函数 │ └── browser.js # 浏览器配置 ├── tests/ │ ├── msw-setup.js # Playwright MSW配置 │ ├── appointment.spec.js │ └── user.spec.js └── playwright.config.js6.2 在团队中推广的经验建立Mock数据契约与后端团队约定API响应格式确保mock数据与真实API一致创建Mock数据生成器// mocks/factories/appointment.js exportconst createMockAppointment (overrides {}) ({ id: APT${Date.now()}, patientName: overrides.patientName || 测试用户, department: overrides.department || internal, doctor: overrides.doctor || 张医生, status: overrides.status || PENDING, appointmentTime: overrides.appointmentTime || 2024-06-15 09:00, createdAt: newDate().toISOString(), ...overrides });可视化Mock管理界面高级需求// 可以创建一个简单的UI来管理mock状态 // 在测试环境中添加一个浮动面板 if (process.env.NODE_ENV development) { // 注入mock控制面板 const mockPanel document.createElement(div); mockPanel.id msw-control-panel; // ... 实现mock状态切换的UI }7. 遇到的坑和解决方案坑1Service Worker缓存问题// 解决方案在测试开始前清理缓存 await page.addInitScript(() { if (serviceWorker in navigator) { navigator.serviceWorker.getRegistrations().then((registrations) { for (const registration of registrations) { registration.unregister(); } }); } });坑2跨域请求拦截失败// 解决方案确保MSW正确处理跨域 export const handlers [ http.get(https://api.example.com/*, () { // 需要完整URL匹配 return HttpResponse.json({ mocked: true }); }) ]; // 或者在Playwright配置中设置baseURL // playwright.config.js use: { baseURL: https://api.example.com, }坑3测试间的状态污染// 解决方案每个测试后重置handlers test.afterEach(async () { server.resetHandlers(); // 同时清除页面状态 await page.evaluate(() { localStorage.clear(); sessionStorage.clear(); }); });8. 效果评估值不值得投入实施MSW三个月后我们的数据变化测试执行时间从平均45分钟减少到12分钟测试稳定性因后端不稳定导致的测试失败减少92%开发体验前端开发不再需要启动完整的后端服务测试覆盖率边缘场景的测试覆盖率从30%提升到85%更重要的是我们现在可以轻松测试那些“罕见但重要”的业务场景服务器错误、网络超时、数据边界情况……9. 开始你的MSW之旅如果你也想开始使用MSW我建议从一个小功能开始选择一个API相对独立的模块先mock只读接口GET请求比POST/DELETE更安全建立团队共识确保大家理解为什么要用MSW逐步替换旧的mock方案不要试图一次性重写所有测试记住任何技术方案的目标都是解决问题而不是增加复杂度。MSW在我们项目中成功了因为它确实解决了测试数据控制的痛点。如果你在实施过程中遇到问题或者有更好的实践方案欢迎随时交流——在测试这条路上我们都在不断学习和改进。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询