
1. 项目概述为什么我们需要融合UI与接口自动化如果你做过一段时间的自动化测试大概率会遇到这样的场景一个完整的业务流程前半段需要登录、填写表单、上传文件这些操作依赖UI界面后半段则涉及到订单状态查询、数据校验、消息通知这些功能通过调用后端接口来完成更高效。传统的做法是UI自动化用Selenium或Playwright写一套脚本接口自动化用Requests或Pytest写另一套脚本。两套脚本独立运行数据难以共享状态无法衔接维护起来简直是灾难。更头疼的是当UI因为一个元素的轻微变动而失败时整个端到端的测试流程就中断了你无法验证后续的接口逻辑是否正确。这正是“用Playwright实现接口自动化与UI自动化的无缝融合”这个项目要解决的核心痛点。它不是一个简单的工具使用教程而是一种测试架构思想的实践。其核心价值在于利用Playwright这一个工具统一测试执行上下文让UI操作和HTTP请求在同一个会话、同一个状态管理下协同工作。想象一下你可以先用Playwright模拟用户登录拿到Cookie然后用这个Cookie直接去调用需要鉴权的API接口进行数据准备接着再回到浏览器界面进行UI操作验证结果整个过程一气呵成数据流和状态流完全打通。这套方案特别适合测试现代Web应用尤其是那些前端交互复杂、后端API众多的单页面应用SPA或中后台管理系统。对于测试开发工程师、全栈开发者以及任何需要保证Web应用质量的同学来说掌握这种融合能力意味着你能设计出更健壮、更高效、也更贴近真实用户场景的自动化测试用例。接下来我会从设计思路拆解开始带你一步步实现这套融合框架。2. 整体设计与核心思路拆解在开始写代码之前理清思路至关重要。我们不是简单地把两个东西拼在一起而是要思考如何让它们“112”。2.1 为什么选择Playwright作为融合的核心市面上UI自动化工具不少Selenium历史悠久Cypress对前端友好。但我选择Playwright作为融合基座主要基于以下几个无可替代的优势原生的网络请求拦截与模拟能力这是实现融合的技术基石。Playwright的page.route()和browserContext.route()方法允许你在浏览器发起真实请求之前就拦截并修改或直接响应它。这意味着你可以在UI测试中“注入”一个接口测试。反过来Playwright也提供了直接发送HTTP请求的API如page.request或context.request让你能在浏览器上下文之外以编程方式调用接口并且自动携带浏览器中已有的Cookie等认证状态。统一的执行上下文与状态共享Playwright的BrowserContext浏览器上下文是一个独立的会话环境它包含了Cookie、本地存储、索引数据库等。无论是通过UI操作page.goto()还是通过API调用context.request.get()只要在同一个BrowserContext下进行所有的状态都是共享的。这完美解决了UI和接口之间状态隔离的问题。强大的自动化与可靠性相比SeleniumPlaywright自动等待机制更智能对现代Web渲染支持更好能有效减少因元素加载时序导致的“flaky tests”不稳定的测试。这保证了融合测试的稳定性。多语言支持与丰富的生态系统支持Python、Node.js、Java、.NET能与Pytest、Jest等主流测试框架无缝集成方便我们构建完整的测试套件。基于以上几点我们的核心设计思路就清晰了以Playwright的BrowserContext为统一沙箱在此沙箱内UI操作Page对象和接口调用API Request对象并行存在并自由交换数据和状态。2.2 融合架构的三种典型模式在实际项目中融合并非千篇一律我根据测试目标总结了三种常用模式接口优先UI验证模式场景需要准备大量测试数据或执行某些UI操作成本很高的前置步骤。流程首先使用Playwright的API请求功能不打开浏览器直接调用后端接口完成数据创建、用户登录态获取等操作。然后利用这些接口返回的数据如ID、Token初始化测试状态再启动浏览器进行UI操作验证数据是否正确展示或流程是否完整。示例测试一个电商订单列表页。先通过接口批量生成10个不同状态的订单然后打开浏览器登录进入订单列表页断言页面上是否正确地显示了这10个订单及其状态。UI触发接口断言模式场景验证前端UI操作是否触发了正确的后端API调用以及API的响应是否符合预期。流程在UI操作如点击提交按钮之前通过page.route()拦截特定的API请求。执行UI操作后在路由处理函数中获取到实际的请求参数和响应对其进行断言。甚至可以修改响应来测试前端对不同API响应的处理逻辑。示例测试一个搜索功能。拦截/api/search这个请求当在搜索框输入关键词并点击搜索后在路由处理函数中断言1) 请求的keyword参数是否正确2) 返回的HTTP状态码是否为200。最后可以放行请求或返回一个模拟的响应数据。混合编排状态接力模式场景复杂的端到端E2E业务流程其中部分环节用UI测试更直观部分环节用接口测试更高效。流程在一个测试用例中交替使用UI操作和接口调用。状态如登录Session、业务ID在整个过程中持续传递。示例测试一个从发布商品到下单的完整流程。步骤1接口调用/api/login接口登录获取auth_token。步骤2UI用Playwright打开浏览器将auth_token写入Cookie导航到商品管理页。步骤3UI在页面上填写表单点击“发布”按钮。步骤4接口通过拦截请求或直接调用查询接口获取刚发布的商品ID。步骤5UI用另一个浏览器上下文模拟另一个用户使用商品ID直接生成商品详情页URL并访问进行下单UI操作。步骤6接口调用订单查询接口验证订单状态是否变为“已支付”。选择哪种模式取决于你的测试用例目标和系统特点。很多时候一个用例里会混合使用多种模式。3. 核心细节解析与实操要点理解了设计思路我们深入到Playwright实现融合的几个核心技术细节。这部分是能否玩转融合测试的关键。3.1 理解并驾驭BrowserContext与APIRequestContext这是两个最重要的对象务必厘清它们的关系和用法。BrowserContext你可以把它想象成一个独立的“隐身模式”浏览器会话。每个Context都有独立的Cookie、本地存储、缓存和证书。当你用browser.newContext()创建它时就得到了一个干净的沙箱。所有在该Context下创建的Page页面和发起的APIRequest都共享这个沙箱的状态。这是实现状态共享的核心。APIRequestContext这是Playwright提供的、用于发送HTTP请求的接口。它可以通过browserContext.request或playwright.request来创建。关键区别在于browserContext.request强烈推荐在融合测试中使用。它自动继承并管理对应BrowserContext的所有状态如Cookie。你在UI页面里登录后用这个request对象发起的API调用自动就带上了登录态。playwright.request这是一个独立的、不绑定任何浏览器上下文的请求对象。适用于纯接口测试或者需要完全独立会话的场景。实操要点 在编写测试类或Fixture时我习惯先初始化一个BrowserContext然后从这个Context派生出Page对象用于UI和APIRequestContext对象用于接口。这样它们三位一体状态天然同步。import pytest from playwright.sync_api import Playwright, Browser, BrowserContext, Page, APIRequestContext pytest.fixture(scopefunction) def api_request_context(browser: Browser) - APIRequestContext: # 为每个测试函数创建一个独立的上下文和请求对象 context browser.new_context() request_context context.request yield request_context # 测试结束后关闭上下文会自动清理所有相关页面和状态 context.close() # 在测试用例中你可以同时使用 page 和 api_request_context def test_fusion_example(page: Page, api_request_context: APIRequestContext): # UI操作登录 page.goto(https://example.com/login) page.fill(#username, testuser) page.fill(#password, password123) page.click(button[typesubmit]) # 此时登录成功的Cookie已经存在于共享的BrowserContext中 # 接口操作使用共享了Cookie的request对象调用需要鉴权的API response api_request_context.get(https://api.example.com/user/profile) assert response.ok profile_data response.json() assert profile_data[username] testuser # 你看无需手动处理Token或Cookie一切自动完成3.2 掌握请求拦截与修改route的实战技巧page.route()是实现“UI触发接口断言”模式的利器。它的工作原理是在请求发送到网络之前将其截获你可以选择继续发送、修改后发送、或者直接返回一个模拟响应。一个完整的拦截示例 假设我们要测试文件上传功能并验证上传时调用的API参数。def test_upload_with_api_intercept(page: Page): # 1. 在执行UI操作前先设置路由拦截 page.route(**/api/upload, lambda route: handle_upload_route(route)) # 2. 执行触发请求的UI操作 page.goto(https://example.com/upload) with page.expect_file_chooser() as fc_info: page.click(input[typefile]) file_chooser fc_info.value # 注意这里需要准备一个真实的测试文件例如 test_image.jpg file_chooser.set_files(path/to/test_image.jpg) page.click(button#upload-btn) # 3. 路由处理函数 def handle_upload_route(route): request route.request # 断言请求方法 assert request.method POST # 断言请求头中包含 multipart/form-data assert multipart/form-data in request.headers.get(content-type, ) # 获取请求的post data (对于multipart这是一个FormData对象) # Playwright 提供了 request.post_data_buffer 来获取原始数据但解析multipart较复杂。 # 更常见的做法是让请求正常发生但断言它成功了。 # 或者我们可以模拟一个成功的响应让UI流程继续。 # 方案A继续实际请求并断言响应 # route.continue_() # 方案B模拟一个成功的JSON响应用于前端逻辑验证 route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps({code: 0, data: {fileId: mock_file_123}, msg: success}) ) # 记录或进行更多断言 print(f拦截到上传请求: {request.url})注意事项与避坑指南匹配模式**/api/upload中的**是通配符匹配任何路径下的/api/upload。你也可以用正则表达式如re.compile(r.*/api/v1/orders/.*)来匹配动态路径。route.continue_()vsroute.fulfill()continue_()是放行请求到真实服务器fulfill()是直接返回一个模拟响应请求不会到达服务器。选择哪个取决于你的测试目的。如果想验证后端逻辑用continue_()并检查真实响应如果只想验证前端行为或模拟特定场景如网络错误、慢响应用fulfill()。异步处理如果路由处理函数中有异步操作如从数据库读取模拟数据记得使用async/await。上面的例子是同步API如果使用异步APIplaywright.async_api写法略有不同。避免死循环不要在路由处理函数中又发起一个会被同一路由规则匹配的请求这会导致无限循环。清理路由使用page.unroute()可以在不需要时移除路由避免对后续测试产生干扰。通常一个测试用例设置的路由在本用例结束时随着page关闭而自动失效但如果page被复用则需要手动清理。3.3 处理认证与状态保持在融合测试中处理登录状态是最常见的需求。我们的目标是一次认证UI和接口共用。最佳实践通过接口登录状态注入Context对于需要登录的测试我强烈建议首先通过API完成登录。因为API登录通常更快、更稳定不受前端UI变化的影响。def test_shared_auth_state(playwright: Playwright): browser playwright.chromium.launch(headlessFalse) # 1. 创建一个干净的浏览器上下文 context browser.new_context() # 2. 创建该上下文关联的请求对象 api_request context.request # 3. 【关键步骤】通过接口登录获取认证信息 login_response api_request.post(https://api.example.com/auth/login, data{username: test, password: 123}) assert login_response.ok auth_token login_response.json().get(token) # 假设后端通过Authorization头认证 # 4. 【关键步骤】将认证信息设置到请求对象的默认头中 # 这样后续所有通过这个api_request发起的调用都会自动携带token api_request.set_extra_http_headers({Authorization: fBearer {auth_token}}) # 同时这个token也可能需要设置到浏览器上下文中供UI使用如果前端通过localStorage或Cookie鉴权 # 例如通过evaluate将token存入localStorage page context.new_page() page.add_init_script(f window.localStorage.setItem(auth_token, {auth_token}); ) # 5. 现在你可以进行任何需要登录态的UI或接口操作了 # 接口操作获取用户信息 profile_resp api_request.get(https://api.example.com/user/profile) assert profile_resp.ok # UI操作访问需要登录的页面 page.goto(https://example.com/dashboard) # 页面应该能正常加载因为localStorage里已经有了token browser.close()这种方法的好处是登录逻辑集中处理且不依赖UI。即使登录页改版也只需要更新接口请求的数据而不会影响成百上千个依赖登录的测试用例。4. 实操过程构建一个融合测试框架理论说再多不如动手搭一个。下面我将以Python Pytest为例展示如何搭建一个基础但实用的融合测试框架。4.1 项目结构与依赖安装首先初始化项目并安装核心依赖。# 创建项目目录 mkdir playwright-fusion-framework cd playwright-fusion-framework # 初始化Python虚拟环境推荐 python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # 安装核心依赖 pip install pytest playwright # 安装Playwright浏览器Chromium, Firefox, WebKit playwright install chromium项目目录结构建议如下playwright-fusion-framework/ ├── conftest.py # Pytest全局配置定义核心fixture ├── requirements.txt # 项目依赖 ├── pages/ # Page Object模型目录 │ ├── __init__.py │ ├── login_page.py │ └── dashboard_page.py ├── api/ # API客户端封装目录 │ ├── __init__.py │ └── user_api.py ├── tests/ # 测试用例目录 │ ├── __init__.py │ ├── test_login_fusion.py │ └── test_order_flow.py └── utils/ # 工具函数 ├── __init__.py └── data_helper.py4.2 编写核心Fixtureconftest.pyconftest.py是Pytest的魔力所在我们在这里定义所有测试用例共享的“装备”。# conftest.py import pytest from playwright.sync_api import Playwright, Browser, BrowserContext, Page, APIRequestContext import os pytest.fixture(scopesession) def playwright_instance() - Playwright: 会话级别的Playwright实例整个测试会话只启动一次。 from playwright.sync_api import sync_playwright with sync_playwright() as playwright: yield playwright pytest.fixture(scopesession) def browser(playwright_instance: Playwright) - Browser: 会话级别的浏览器实例。 # 可以根据环境变量决定是否无头运行、使用哪个浏览器 headless os.getenv(HEADLESS, true).lower() true browser playwright_instance.chromium.launch(headlessheadless, slow_mo500) # slow_mo让操作变慢方便调试 yield browser browser.close() pytest.fixture(scopefunction) def context(browser: Browser) - BrowserContext: 函数级别的浏览器上下文。每个测试函数一个独立的隔离环境。 # 可以在这里配置上下文选项如视口大小、语言、权限等 context browser.new_context( viewport{width: 1920, height: 1080}, localezh-CN, # 忽略HTTPS错误仅测试环境使用 ignore_https_errorsTrue ) yield context context.close() pytest.fixture(scopefunction) def page(context: BrowserContext) - Page: 函数级别的页面对象。 page context.new_page() yield page page.close() pytest.fixture(scopefunction) def api_request(context: BrowserContext) - APIRequestContext: 【核心】函数级别的API请求对象与context共享状态。 # 直接从共享的context中创建request对象 request context.request # 可以在这里设置一些全局请求头如Content-Type request.set_extra_http_headers({ Content-Type: application/json, }) yield request # request对象不需要显式关闭它会随context一起销毁。这个配置为每个测试函数提供了一个干净的沙箱context以及在这个沙箱中工作的两个工具操作页面的page和调用接口的api_request。它们的状态完全同步。4.3 封装Page Object与API Client为了代码更清晰、可维护我们需要对UI操作和接口调用进行分层封装。封装Page Object(pages/login_page.py)# pages/login_page.py from playwright.sync_api import Page class LoginPage: def __init__(self, page: Page): self.page page self.username_input page.locator(#username) self.password_input page.locator(#password) self.submit_button page.locator(button[typesubmit]) self.error_message page.locator(.alert-error) def navigate(self): self.page.goto(https://example.com/login) def login(self, username: str, password: str): self.username_input.fill(username) self.password_input.fill(password) self.submit_button.click() def get_error_message(self) - str: return self.error_message.text_content() or 封装API Client(api/user_api.py)# api/user_api.py from typing import Optional, Dict, Any from playwright.sync_api import APIRequestContext class UserApiClient: def __init__(self, request: APIRequestContext, base_url: str https://api.example.com): self.request request self.base_url base_url.rstrip(/) def login(self, username: str, password: str) - Dict[str, Any]: 通过API登录返回响应数据。 response self.request.post( f{self.base_url}/auth/login, data{username: username, password: password} ) response.raise_for_status() # 如果状态码不是2xx抛出异常 return response.json() def get_profile(self) - Dict[str, Any]: 获取当前用户资料需要登录态。 response self.request.get(f{self.base_url}/user/profile) response.raise_for_status() return response.json() def update_profile(self, data: Dict) - Dict[str, Any]: 更新用户资料。 response self.request.put(f{self.base_url}/user/profile, datadata) response.raise_for_status() return response.json()4.4 编写融合测试用例现在我们可以编写一个真正的融合测试用例了。这个用例将演示“接口优先UI验证”模式。# tests/test_login_fusion.py import pytest from pages.login_page import LoginPage from api.user_api import UserApiClient class TestLoginFusion: 测试登录功能的融合场景。 def test_login_via_api_then_verify_ui(self, page: Page, api_request: APIRequestContext): 场景通过API完成登录然后打开UI页面验证UI上显示的用户信息与API返回的一致。 模式接口优先UI验证。 # 1. 初始化API客户端和Page Object user_api UserApiClient(api_request, base_urlhttps://api.example.com) login_page LoginPage(page) # 2. 【接口操作】通过API登录获取用户数据和认证状态 login_data user_api.login(usernametest_user, passwordsecure_pass_123) assert login_data[code] 0, fAPI登录失败: {login_data} auth_token login_data[data][token] user_info_from_api login_data[data][user] # 3. 【状态同步】将认证Token注入到Page的上下文中假设前端通过localStorage校验 # 这步是关键让UI页面能识别出已登录状态 page.add_init_script(f window.localStorage.setItem(auth_token, {auth_token}); window.localStorage.setItem(user_info, JSON.stringify({user_info_from_api})); ) # 4. 【UI操作】导航到需要登录后才能访问的仪表盘页面 # 注意这里不是去登录页而是直接去受保护的页面 page.goto(https://example.com/dashboard) # 5. 【UI验证】在页面上断言用户信息正确显示 # 假设仪表盘页有一个元素显示用户名 welcome_element page.locator(.welcome-message) # 等待元素出现并获取文本 welcome_text welcome_element.text_content() assert user_info_from_api[name] in welcome_text, fUI显示的用户名[{welcome_text}]与API返回的[{user_info_from_api[name]}]不符 # 假设有一个用户头像其src属性包含用户ID avatar_element page.locator(.user-avatar img) avatar_src avatar_element.get_attribute(src) assert str(user_info_from_api[id]) in avatar_src, f头像链接不包含用户ID: {avatar_src} # 6. 可选【二次接口验证】再次调用API确保UI操作没有破坏登录态 profile_after_ui user_api.get_profile() assert profile_after_ui[data][id] user_info_from_api[id], UI操作后API登录态异常 def test_ui_login_intercept_api(self, page: Page, api_request: APIRequestContext): 场景在UI登录过程中拦截登录API请求验证请求参数并模拟一个自定义响应。 模式UI触发接口断言并模拟。 login_page LoginPage(page) login_page.navigate() # 在点击登录按钮前先拦截登录API intercepted_request_data {} def intercept_login_route(route): request route.request intercepted_request_data[url] request.url intercepted_request_data[method] request.method # 尝试获取POST数据JSON格式 try: intercepted_request_data[post_data] request.post_data_json except: intercepted_request_data[post_data] None # 关键这里我们不继续真实请求而是直接返回一个模拟的成功响应 # 这可以用于测试前端对特定响应如新用户引导的处理逻辑 route.fulfill( status200, headers{Content-Type: application/json}, body{code: 0, data: {token: mock_token_999, user: {name: MockUser}}, msg: 登录成功模拟} ) # 开始路由监听 page.route(**/auth/login, intercept_login_route) # 执行UI登录操作 login_page.login(usernametest, passwordtest) # 断言拦截到的请求参数 assert intercepted_request_data[method] POST assert intercepted_request_data[post_data] {username: test, password: test} # 断言UI根据模拟响应做出了正确反应例如跳转到了引导页 # 注意因为我们模拟了响应所以实际后端并未收到请求这里验证的是前端逻辑 page.wait_for_url(**/guide) # 假设模拟响应会让前端跳转到引导页 assert page.locator(h1:has-text(欢迎新用户)).is_visible()这个测试类展示了两种典型的融合模式。第一个测试用例test_login_via_api_then_verify_ui高效且稳定适合用于冒烟测试或核心流程验证。第二个测试用例test_ui_login_intercept_api则更侧重于前端逻辑和异常场景的验证。5. 常见问题、排查技巧与性能优化在实际项目中落地融合测试你会遇到各种挑战。下面是我踩过坑后总结的一些经验和技巧。5.1 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案接口调用返回403/401未授权API请求对象api_request与UI页面page不在同一个BrowserContext下状态Cookie未共享。1. 检查fixture确保api_request是从contextfixture创建的context.request而不是playwright.request。2. 检查登录逻辑确保登录API调用和后续操作使用的是同一个api_request对象。page.route()拦截不到请求1. 路由注册时机太晚请求已经发出。2. URL匹配模式不正确。3. 请求是从iframe或Worker发出的需要特殊处理。1.务必在触发请求的UI操作如page.click()之前调用page.route()。最好在页面导航page.goto()前就设置好。2. 使用page.on(“request”, handler)监听所有请求打印出URL核对你的匹配模式。3. 对iframe使用frame.route()对全局使用browserContext.route()。模拟响应fulfill后页面行为异常模拟的响应数据格式或结构与真实响应不一致导致前端JS解析错误。1. 使用浏览器开发者工具的“网络”面板抓取一次真实的API响应完整复制其Headers和Body。2. 在route.fulfill()中尽可能真实地模拟包括content-type等头部信息。3. 检查前端控制台是否有JS报错。UI操作后接口状态丢失可能因为页面跳转导航到了不同源domain的地址导致浏览器上下文切换。1. 对于同源跳转状态通常会保留。2. 对于跨域需要检查目标站点是否支持单点登录SSO或是否有其他状态传递机制。在测试环境中可以考虑配置--disable-web-security启动浏览器仅用于测试或使用代理将所有请求导向同一测试域名。测试运行速度慢1. 浏览器以有头模式启动。2. 每个测试都启动新浏览器。3. 网络请求或操作缺少等待使用time.sleep导致空等。1. 在CI环境或追求速度时设置headlessTrue。2. 使用scopesession的browserfixture整个测试会话只启动一次浏览器。3.使用Playwright内置的等待page.wait_for_load_state(“networkidle”),locator.wait_for()避免硬性等待。元素定位失败但页面看似已加载现代前端框架React, Vue动态渲染元素出现时机晚于load事件。1. 使用page.wait_for_selector()或locator.wait_for()等待特定元素出现。2. 使用page.wait_for_function()等待某个JS条件成立如window.dataLoaded true。3. 增加超时时间page.click(“selector”, timeout10000)。5.2 性能优化与最佳实践Fixture作用域管理playwright_instance和browser使用scopesession避免重复启动Playwright和浏览器进程这是最大的性能提升点。context和page使用scopefunction保证测试之间的隔离性。虽然创建新的Context有一定开销但远小于启动新浏览器且能保证测试纯净。并行测试 Playwright支持并行测试。在Pytest中可以使用pytest-xdist插件。关键点并行时每个Worker进程需要有自己的Browser实例。我们的Fixture配置browser是session级别在默认情况下可能不兼容。更安全的做法是将browserfixture也设置为scopefunction或使用pytest.fixture(scopeclass)但这会牺牲一些性能。或者使用Playwright提供的playwright.chromium.launch_server()启动浏览器服务器让多个测试进程连接同一个浏览器实例更高级需仔细处理隔离。视频与追踪录制 在调试难以复现的问题时开启视频录制和追踪Trace是救命稻草。可以在contextfixture中配置pytest.fixture(scopefunction) def context(browser): context browser.new_context( record_video_dirvideos/, record_video_size{width: 1920, height: 1080} ) # 或者更强大的追踪功能 # context.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue) yield context # 测试失败时保存追踪文件 # if request.node.rep_call.failed: # context.tracing.stop(pathftrace_{request.node.name}.zip) context.close()记得在CI流水线中清理或上传这些产物。数据驱动与测试隔离 融合测试涉及UI和接口对测试数据的要求更高。务必保证每个测试用例使用独立的数据避免用例间相互影响。可以通过在接口准备阶段创建带唯一标识的数据如ftest_order_{uuid.uuid4().hex[:8]}并在测试后通过接口清理teardown来实现。选择正确的等待策略网络请求page.wait_for_load_state(“networkidle”)等待页面主要资源加载完成。元素状态locator.wait_for(state”visible”)等待元素可见。自定义条件page.wait_for_function()等待前端JS状态。绝对避免除非万不得已不要使用time.sleep()。它不可靠且低效。5.3 一个真实的排错案例Cookie域不匹配我曾遇到一个棘手问题通过api_request登录成功Cookie也已存在context中但导航到UI页面后页面显示未登录。排查过程首先我打印了登录后context中的所有Cookieprint(context.cookies())。发现Cookie的domain字段是.api.example.com。然后我导航到的UI页面URL是https://www.example.com/dashboard。问题浮出水面Cookie的域.api.example.com与UI页面的域www.example.com不完全匹配。浏览器出于安全原因SameSite策略不会将.api.example.com的Cookie发送到www.example.com。解决方案方案一推荐治本协调开发将API和前端主站部署在相同的顶级域下例如API用https://example.com/api前端用https://example.com这样Cookie可以共享domain设置为.example.com。方案二测试环境变通在启动测试浏览器时通过--host-resolver-rules参数或修改系统的hosts文件将www.example.com和api.example.com都映射到同一个IP如测试服务器IP这样浏览器会认为它们是同一个站点SchemeDomainPort相同。方案三仅限简单测试如果前端支持在登录API响应中除了返回Token也返回用户信息。然后通过page.add_init_script直接将用户信息写入localStorage或sessionStorage完全绕过Cookie。正如我们在示例代码中所做的那样。这个案例告诉我们在融合测试中对HTTP基础如Cookie、同源策略的理解至关重要。当状态共享失败时第一步就是检查网络请求和响应的头部信息以及浏览器开发者工具中Application标签页下的Storage状态。