Playwright Java网络拦截与API测试实战:从Mock到集成测试
1. 项目概述为什么我们需要网络拦截与API测试如果你已经用Playwright for Java写过一些基础的UI自动化脚本比如点击按钮、填写表单那你可能已经感受到了它的强大和稳定。但自动化测试的世界远不止于此。当你的应用变得越来越复杂前后端分离、微服务架构成为常态时仅仅验证页面元素是否可见、按钮能否点击就像是只检查了汽车的油漆和轮胎而忽略了发动机和变速箱的运转。真正的质量保障需要深入到应用与服务器之间的每一次“对话”中去。这就是网络拦截Network Interception和API测试API Testing的用武之地。想象一下你的前端页面加载了一个用户列表这个列表数据是通过调用/api/users这个接口获取的。传统的UI测试只能断言页面上最终是否显示了“张三”和“李四”。但如果这个接口返回了错误的数据结构或者响应时间长达10秒UI测试可能直到最后一步才会失败甚至因为超时机制而报出令人困惑的错误。而通过网络拦截你可以在请求发出和响应返回的瞬间就“截获”并检查它们。你可以断言请求的URL、方法、请求头、请求体是否正确也可以模拟服务器的响应甚至直接修改响应内容来测试前端的各种处理逻辑。API测试则更进一步它允许你脱离浏览器直接以代码的方式模拟客户端去调用这些接口验证其功能、性能、安全性和可靠性。将两者结合在Playwright的框架下你就能构建起一套从用户界面到后端接口的、立体的、可观测的自动化测试体系。这不仅能更快地发现缺陷定位问题的根因是前端传参错了还是后端逻辑有问题还能在UI尚未开发完成时就提前对接口进行验证真正实现测试左移。我最近在一个电商项目中实践了这套组合拳。我们有一个“提交订单”的功能UI测试很稳定但偶尔在促销高峰期会失败。通过增加网络拦截我们发现了问题前端在请求中漏传了一个关键的促销活动ID而后端在特定负载下对缺失该字段的请求处理不当。如果没有拦截到这次网络请求我们可能需要花费数小时去查看日志、猜测复现路径。这个实战经验让我坚信掌握Playwright的高级网络能力是从“脚本录制员”迈向“测试开发工程师”的关键一步。2. 核心能力拆解Playwright网络API全景图在开始实战之前我们有必要系统性地了解一下Playwright为我们提供的网络相关API。它们主要分布在Page、Route和Request/Response这几个核心对象上形成了一个完整的工作流。2.1 核心对象与生命周期一个完整的网络请求在Playwright视角下的生命周期是这样的请求发起页面中的代码如JavaScript的fetch或XMLHttpRequest发起一个网络请求。路由匹配与拦截Playwright检查该请求是否匹配你预先设置的“路由”Route规则。如果匹配请求会被暂停控制权交给你编写的处理函数。请求处理在你的处理函数中你可以访问到Request对象查看其URL、方法、头信息、POST数据等。此时你有几个选择继续Fulfill你可以选择让请求继续发往真正的服务器或者使用自定义的响应如一个Mock数据来“满足”fulfill这个请求从而绕过服务器。中止Abort你也可以直接中止这个请求模拟网络失败或资源加载失败的情况。响应处理如果请求被继续发往服务器并返回了响应你会收到Response对象。你可以监听响应事件来检查状态码、响应头、响应体等信息。整个流程的核心是page.route(url, handler)方法。url可以是一个字符串、通配符或正则表达式用于匹配请求的URL。handler是一个函数它接收一个Route对象作为参数你通过这个对象来决定请求的命运。2.2 关键API详解page.route(url, handler)/page.route(from, to): 这是设置拦截的起点。第一个参数是匹配模式第二个是处理函数。还有一个重载形式page.route(from, to)可以用于简单的URL重写例如将指向测试环境的请求重定向到本地Mock服务器。route.request(): 在handler中通过route.request()获取被拦截的Request对象。这是你了解“谁在请求什么”的关键。route.fulfill([options]): 这是拦截后最常用的操作。你可以直接返回一个自定义的响应而不再将请求发往真实服务器。options中你可以指定状态码status、响应头headers、响应体body或path等。这是实现Mock的核心。route.continue([overrides]): 让请求继续发往服务器但你可以在overrides中覆盖原始的请求参数比如修改请求头、POST数据。这常用于测试服务器对不同输入的处理。route.abort([errorCode]): 中止请求模拟失败。errorCode可以是‘aborted’,‘accessdenied’,‘connectionclosed’等用于模拟不同的网络错误场景。page.on(“request”, handler)/page.on(“response”, handler): 这是事件监听器与route不同。它们不会拦截或暂停请求只是在请求发生或响应返回时通知你允许你进行记录、断言或性能监控。通常用于监控和收集信息而非改变请求流程。request.postData()/response.body(): 用于获取请求体和响应体。注意response.body()返回的是二进制Buffer对于JSON响应你需要先将其转换为字符串再解析为JSON对象。APIRequestContext: 这是Playwright进行无头API测试的独立模块。你可以脱离浏览器页面直接创建一个请求上下文用来发送HTTP请求并接收响应。它支持Cookie、Header的持久化非常适合做纯粹的接口自动化测试或为UI测试准备测试数据。理解这些API的职责和区别至关重要。混淆route.fulfill拦截并模拟和page.on(“response”)仅监听是新手常犯的错误会导致脚本行为与预期不符。3. 实战演练一拦截与修改网络请求理论说得再多不如一行代码。让我们从一个最常见的场景开始修改请求。3.1 场景为所有请求添加认证令牌假设你的应用在每个向API发起的请求头中都需要携带一个Authorization: Bearer token。在测试环境中你可能不想每次都走完整的登录流程来获取一个有效的token。这时拦截并添加请求头就非常方便。import com.microsoft.playwright.*; import java.util.regex.Pattern; public class AddAuthHeaderExample { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { Browser browser playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)); BrowserContext context browser.newContext(); Page page context.newPage(); // 拦截所有向 /api/ 路径发起的请求 page.route(Pattern.compile(.*/api/.*), route - { // 1. 获取原始请求 Request request route.request(); System.out.println(“拦截到请求: ” request.method() ” ” request.url()); // 2. 创建新的请求头复制旧的并添加认证头 // 注意Playwright的Headers是MapString, String // 直接修改route.request().headers()返回的Map是无效的需要通过continue()覆盖 // 但更简单的做法是在continue()的overrides参数中直接设置新headers // 这里演示如何构建一个新的headers map // 在实际中你的token可能从环境变量或配置文件中读取 String testToken “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...”; // 示例Token // 3. 让请求继续但覆盖其请求头 route.continue_( new Route.ContinueOverrides() .setHeaders( // 将原始headers转换并添加新条目 // 注意这里简单演示实际应合并原有headers request.headers() ) // 更稳妥的做法是创建一个新的Map放入所有需要的headers // 因为setHeaders会完全替换headers而不是合并 ); // 上面的写法会丢失原始headers正确做法如下 MapString, String headers new HashMap(request.headers()); // 复制原始headers headers.put(“Authorization”, “Bearer ” testToken); // 添加新header route.continue_(new Route.ContinueOverrides().setHeaders(headers)); }); // 导航到页面页面内发起的任何匹配 /api/ 的请求都会被自动加上Token page.navigate(“https://your-app.com”); // ... 后续测试操作 page.waitForTimeout(5000); // 等待观察 browser.close(); } } }关键点与避坑指南Pattern.compile的使用page.route()的第一个参数可以是字符串支持通配符*但更强大的是使用Pattern对象进行正则表达式匹配。Pattern.compile(“.*/api/.*”)会匹配所有URL中包含/api/的请求。这比简单的字符串匹配更灵活。请求头的修改方式这是一个极易出错的地方。route.request().headers()返回的是一个不可变的Map视图。你不能直接修改它然后什么都不做这样是无效的。你必须通过route.continue_(new Route.ContinueOverrides().setHeaders(newHeaders))来替换整个请求头集合。因此安全的做法是先复制原始headers到一个新的可变Map中进行修改然后用这个新Map去设置。continue_方法名注意在Java API中由于continue是关键字所以方法名是continue_带下划线。作用域page.route()设置的拦截器对该页面生效。如果你在BrowserContext级别设置 (context.route())则对该上下文中的所有页面都生效这常用于设置全局的Mock规则。3.2 场景拦截并模拟MockAPI响应这是网络拦截最核心的用途之一。比如你想测试前端在“商品库存为0”时的展示逻辑但后台数据库里总是有库存。这时直接Mock一个库存为0的响应是最快的方式。public class MockApiResponseExample { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { Browser browser playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)); Page page browser.newPage(); // 拦截获取商品详情的特定API page.route(“**/api/product/123”, route - { System.out.println(“Mocking response for product 123”); // 构建一个模拟的JSON响应体 String mockJsonBody “{\”id\”: 123, \”name\”: \”测试商品\”, \”price\”: 99.9, \”stock\”: 0}”; // 库存为0 // 使用 route.fulfill() 直接返回模拟响应 route.fulfill(new Route.FulfillOptions() .setStatus(200) // HTTP 200 OK .setContentType(“application/json”) // 设置正确的Content-Type .setBody(mockJsonBody) // 设置响应体 ); // 请求到此为止不会发送到真实服务器 }); page.navigate(“https://your-app.com/product/123”); // 此时页面收到的商品数据就是stock:0你可以接着断言页面是否显示了“缺货”标签 // 例如assertThat(page.locator(“.out-of-stock-badge”)).isVisible(); page.waitForTimeout(3000); browser.close(); } } }实操心得**通配符在匹配模式“**/api/product/123”中**匹配任何字符序列包括路径分隔符。这意味着无论这个API的完整路径是什么如https://api.test.com/v1/api/product/123或https://app.com/api/product/123都能被匹配到。这比写死域名要灵活得多适配不同环境。设置Content-Type这一点至关重要。如果你Mock的是一个JSON API但忘记设置ContentType为“application/json”前端JavaScript可能无法正确解析响应体导致测试失败。永远根据真实API的响应头来设置你的Mock头信息。状态码除了成功的200你也应该测试前端对错误状态码如404、500、401的处理。只需在setStatus()中修改即可。动态Mock你的Mock数据不必是硬编码的字符串。可以从文件读取、根据请求参数动态生成甚至使用像Faker这样的库来生成逼真的测试数据。4. 实战演练二监听与断言网络活动拦截是为了改变而监听是为了观察和验证。在很多情况下我们不需要修改请求/响应只需要确保它们按预期发生。4.1 场景验证页面加载的关键资源假设你的产品经理要求首页必须加载某个关键的统计分析SDK比如analytics.js。你可以通过监听“request”事件来验证。public class MonitorRequestsExample { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { Browser browser playwright.chromium().launch(); Page page browser.newPage(); // 使用一个列表来收集匹配的请求 ListString capturedUrls new ArrayList(); // 添加请求监听器 page.onRequest(request - { String url request.url(); if (url.contains(“analytics.js”)) { // 简单的包含匹配也可以用正则 System.out.println(“捕获到关键资源请求: ” url); capturedUrls.add(url); } }); // 导航到首页 page.navigate(“https://your-app.com”); // 等待页面可能发生的异步加载 page.waitForLoadState(LoadState.NETWORKIDLE); // 进行断言 assertThat(capturedUrls).as(“应加载统计分析SDK”).isNotEmpty(); // 更精确的断言 assertThat(capturedUrls).anyMatch(u - u.contains(“https://cdn.analytics-provider.com/analytics.v3.js”)); browser.close(); } } }4.2 场景断言API调用的请求参数和响应这是API测试集成到UI测试中的精华。你不仅关心页面表现还关心背后的数据交互是否正确。public class AssertApiCallExample { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { Browser browser playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)); Page page browser.newPage(); // 准备一个Promise来捕获我们关心的响应 // Playwright Java 中我们可以使用 page.waitForResponse 更简洁 // 但为了演示监听器模式我们先使用事件监听 // 更佳实践使用 page.waitForResponse 等待特定响应 // 它返回一个Response对象可以链式调用进行断言 page.navigate(“https://your-app.com/login”); // 在触发动作前先设置一个等待响应的Promise // 注意waitForResponse 接受一个谓词lambda来匹配响应 Response loginResponse page.waitForResponse(response - response.url().contains(“/api/login”) response.status() 200, () - { // 在等待响应期间执行会触发该请求的动作 page.locator(“input[name’username’]”).fill(“testuser”); page.locator(“input[name’password’]”).fill(“password123”); page.locator(“button[type’submit’]”).click(); } ); // 现在可以直接对Response对象进行断言 assertThat(loginResponse.status()).isEqualTo(200); // 断言响应体包含特定内容 String responseBody new String(loginResponse.body()); assertThat(responseBody).contains(“\”success\”:true”); // 如果你想检查请求的payload需要结合route或request监听 // 但更常见的做法是在waitForResponse的回调中你已经触发了请求 // 如果需要断言请求体可以这样做 page.onRequest(request - { if (request.url().contains(“/api/login”) “POST”.equals(request.method())) { String postData request.postData(); assertThat(postData).contains(“testuser”); assertThat(postData).contains(“password123”); } }); // 注意onRequest是监听器会触发多次需要小心处理断言避免重复断言或干扰。 browser.close(); } } }重要提示page.waitForResponse()是一个极其有用的方法。它等待一个匹配特定条件的响应出现并且你可以将触发该请求的操作如click()放在它的Runnable参数中。这样保证了“操作-等待-断言”的时序正确性避免了使用page.on(“response”, ...)监听器时可能遇到的竞态条件操作执行了但监听器还没注册上。5. 实战演练三独立的API测试使用APIRequestContextPlaywright不仅仅是一个浏览器自动化工具它的APIRequestContext让你能像使用HttpClient或RestAssured一样进行纯粹的、无浏览器的API测试。这对于测试后端微服务、准备测试数据、或者在UI测试前确保API状态正确都非常有用。5.1 环境搭建与基础请求首先你需要在项目的pom.xml中确保引入了Playwright的依赖。dependency groupIdcom.microsoft.playwright/groupId artifactIdplaywright/artifactId version1.40.0/version !-- 使用最新版本 -- /dependency然后让我们看一个基础的GET和POST示例import com.microsoft.playwright.*; import com.microsoft.playwright.options.RequestOptions; import java.nio.file.Paths; public class StandaloneApiTest { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { // 1. 创建API请求上下文可以在此配置全局的baseURL、headers等 APIRequestContext requestContext playwright.request().newContext( new APIRequest.NewContextOptions() .setBaseURL(“https://jsonplaceholder.typicode.com”) // 设置基础URL .setExtraHTTPHeaders(Map.of( “Content-Type”, “application/json”, “User-Agent”, “Playwright-API-Test” )) ); // 示例1: GET 请求 System.out.println(“--- GET Request ---”); APIResponse getResponse requestContext.get(“/posts/1”); System.out.println(“Status: ” getResponse.status()); System.out.println(“Body: ” getResponse.text()); assertThat(getResponse.status()).isEqualTo(200); assertThat(getResponse.json()).isNotNull(); // 示例2: POST 请求 (发送JSON) System.out.println(“\n--- POST Request ---”); String postJson “{\”title\”: \”foo\”, \”body\”: \”bar\”, \”userId\”: 1}”; APIResponse postResponse requestContext.post(“/posts”, RequestOptions.create() .setData(postJson) // 设置请求体 ); System.out.println(“Status: ” postResponse.status()); System.out.println(“Body: ” postResponse.text()); assertThat(postResponse.status()).isEqualTo(201); // 创建成功通常是201 assertThat(postResponse.json().get(“id”)).isNotNull(); // 示例3: 上传文件 (Multipart Form Data) System.out.println(“\n--- File Upload ---”); // 假设我们有一个测试文件 APIResponse uploadResponse requestContext.post(“/upload”, RequestOptions.create() .setMultipart( Map.of( “file”, Paths.get(“./testfile.txt”) // 文件字段 // “fieldName”, “fieldValue” // 可以同时传其他字段 ) ) ); System.out.println(“Upload Status: ” uploadResponse.status()); // 2. 记得关闭请求上下文 requestContext.dispose(); } catch (Exception e) { e.printStackTrace(); } } }5.2 高级功能认证、Cookie与状态保持APIRequestContext的强大之处在于它能够模拟一个真实的客户端会话保持Cookie和认证状态。public class ApiTestWithAuth { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { // 创建一个持久化存储状态的上下文 // 注意Playwright的APIRequestContext默认不自动管理Cookie需要手动设置或从BrowserContext继承 // 但我们可以通过先进行登录请求然后复用返回的Cookie APIRequestContext requestContext playwright.request().newContext(); // 步骤1: 登录获取认证Token或Cookie String loginPayload “{\”username\”: \”test\”, \”password\”: \”123456\”}”; APIResponse loginResponse requestContext.post(“https://your-api.com/login”, RequestOptions.create() .setData(loginPayload) .setHeader(“Content-Type”, “application/json”) ); assertThat(loginResponse.status()).isEqualTo(200); String authToken loginResponse.json().get(“token”).getAsString(); // 假设返回JSON包含token // 步骤2: 使用获取到的Token为后续所有请求设置Authorization头 // 创建一个新的上下文携带认证信息这样更清晰 APIRequestContext authedContext playwright.request().newContext( new APIRequest.NewContextOptions() .setBaseURL(“https://your-api.com”) .setExtraHTTPHeaders(Map.of( “Authorization”, “Bearer ” authToken, “Content-Type”, “application/json” )) ); // 步骤3: 执行需要认证的请求 APIResponse profileResponse authedContext.get(“/api/profile”); assertThat(profileResponse.status()).isEqualTo(200); System.out.println(“Profile: ” profileResponse.text()); // 步骤4: 另一个需要认证的请求 (Cookie/Header会自动携带) APIResponse ordersResponse authedContext.get(“/api/orders”); assertThat(ordersResponse.status()).isEqualTo(200); // 清理 authedContext.dispose(); requestContext.dispose(); } } }经验之谈对于复杂的、有状态的API测试流程如登录 - 查询 - 创建 - 删除我强烈建议为每个独立的测试用例创建新的APIRequestContext并在AfterEach(JUnit) 或AfterTest(TestNG) 方法中dispose()它。这保证了测试之间的隔离避免了脏数据污染。同时将通用的配置如baseURL、默认headers提取到BeforeAll的初始化方法中能大幅提升代码的整洁度。6. 集成测试将网络拦截与UI自动化无缝结合真正的威力在于混合使用。我们可以在一个测试用例中既进行API级别的数据准备和校验又进行UI层面的交互和断言。6.1 场景使用API准备测试数据通过UI验证并用网络拦截确保流程假设我们要测试一个“创建用户并立即在列表中看到”的功能。import org.testng.annotations.Test; import static org.assertj.core.api.Assertions.assertThat; public class IntegratedE2ETest { Test public void testCreateUserAndList() { try (Playwright playwright Playwright.create()) { Browser browser playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false)); BrowserContext context browser.newContext(); Page page context.newPage(); // --- 阶段1: 使用API准备测试环境 (前置条件) --- APIRequestContext api playwright.request().newContext( new APIRequest.NewContextOptions().setBaseURL(“https://your-api.com”) ); // 清理可能存在的旧测试用户 api.delete(“/api/test-users/cleanup?prefixtest_auto”); // 创建一个新的测试用户 String uniqueUsername “test_auto_” System.currentTimeMillis(); String createUserJson String.format( “{\”username\”: \”%s\”, \”email\”: \”%stest.com\”, \”role\”: \”user\”}”, uniqueUsername, uniqueUsername ); APIResponse createResp api.post(“/api/users”, RequestOptions.create() .setHeader(“Authorization”, “Bearer admin-token”) .setData(createUserJson) ); assertThat(createResp.status()).isEqualTo(201); String userId createResp.json().get(“id”).getAsString(); System.out.println(“Created test user with ID: ” userId); api.dispose(); // 数据准备完毕释放API上下文 // --- 阶段2: UI操作与网络拦截断言 --- page.navigate(“https://your-app.com/admin/users”); // 拦截用户列表API断言我们刚创建的用户在其中 // 这里我们使用 waitForResponse 来精确捕获列表加载的响应 page.waitForResponse(response - response.url().contains(“/api/users”) response.status() 200, () - { // 触发列表刷新比如点击搜索按钮或者页面自动加载 page.locator(“button#refresh-user-list”).click(); } ); // 上面的waitForResponse只等待不获取响应体。我们需要监听或再次拦截来获取数据。 // 更直接的方式在点击前设置一个Promise来获取响应体 final String[] listResponseBody new String[1]; page.onResponse(response - { if (response.url().contains(“/api/users”) response.status() 200) { listResponseBody[0] new String(response.body()); } }); page.locator(“button#refresh-user-list”).click(); page.waitForTimeout(1000); // 简单等待一下确保响应被捕获 // 断言响应体包含新创建的用户名 assertThat(listResponseBody[0]).contains(uniqueUsername); // --- 阶段3: 纯UI断言 --- // 在页面上的用户列表中找到该用户的行 Locator userRow page.locator(“table#user-table tr:has-text(‘” uniqueUsername “‘)”); assertThat(userRow).isVisible(); // 可以进一步进行UI操作比如点击编辑、删除等 userRow.locator(“button.delete”).click(); page.locator(“.modal-confirm-btn”).click(); // 验证删除成功可以通过UI提示或者再次拦截列表API assertThat(page.locator(“text‘User deleted successfully’”).first()).isVisible(); browser.close(); } } }这个测试用例展示了完整的“API准备 - UI操作与网络验证 - UI断言”流程。它比纯UI测试更快因为数据准备通过API直接完成也更可靠通过网络拦截直接验证了前后端数据一致性。7. 常见问题、调试技巧与性能优化即使掌握了所有API在实际项目中你依然会遇到各种问题。这里分享一些我踩过的坑和总结的技巧。7.1 常见问题排查表问题现象可能原因解决方案page.route()拦截不生效1. 匹配模式URL写错了。2.route()调用晚于页面导航/请求发起。3. 请求来自 iframe 或 Service Worker。1. 使用System.out.println(request.url())打印实际URL进行比对。使用**通配符。2.务必在page.navigate()之前调用page.route()。3. 对 iframe 使用frame.route()Service Worker 拦截较为复杂通常需要特殊处理。route.fulfill()后页面行为异常1. Mock的响应头如Content-Type不正确。2. 响应体格式不符合前端预期如JSON语法错误。3. 状态码不对。1. 使用浏览器开发者工具的“网络”面板查看真实响应的头信息并完全模拟。2. 使用 JSON 验证工具检查Mock数据格式。对于复杂JSON建议从文件读取或使用对象映射库如Jackson生成。3. 根据API设计返回正确的状态码200, 201, 404等。page.waitForResponse()超时1. 匹配条件谓词太严格或不正确永远等不到。2. 触发请求的操作没有执行或失败了。3. 请求根本没有发生。1. 简化匹配条件先确保能捕获到请求。打印response.url()和response.status()调试。2. 确保触发操作如click()的定位器正确元素可交互。在操作前加page.waitForSelector()。3. 检查前端逻辑确认在测试场景下该请求是否一定会发出。API测试 (APIRequestContext) 返回乱码或解析失败响应编码问题或者response.text()对非文本内容如图片处理不当。对于文本可以尝试response.text(“UTF-8”)指定编码。对于二进制数据使用response.body()获取byte[]。脚本运行速度慢1. 拦截了太多不必要的请求如图片、字体、CSS。2. 使用了waitForTimeout进行固定等待。3. 浏览器以非无头模式启动。1. 精确设置拦截URL模式避免**匹配所有请求。对于不需要的静态资源可以考虑route.abort()或直接忽略。2.永远优先使用事件驱动的等待如waitForSelector,waitForResponse,waitForLoadState代替硬编码的waitForTimeout。3. 在CI/CD环境中始终使用setHeadless(true)。本地调试时可关闭。7.2 调试技巧启用Playwright Debug Log在创建Playwright实例时可以设置环境变量DEBUGpw:api。在Java中可以通过在运行配置中添加-DDEBUGpw:api来实现。这会在控制台打印出Playwright所有的内部API调用对于理解脚本在做什么非常有帮助。结合浏览器开发者工具在非无头模式 (setHeadless(false)) 下运行测试手动打开开发者工具的Network面板。你可以清晰地看到哪些请求被发送了哪些被拦截了请求和响应的具体内容是什么。这是排查拦截和Mock问题最直观的方法。善用console.log和System.out.println在route的handler函数中、page.on的监听器中打印关键信息如URL、状态码、请求体片段。这是定位逻辑错误的最简单手段。使用page.pause()在脚本中插入page.pause()运行时会打开Playwright Inspector你可以单步执行命令查看当前页面状态实时评估定位器是调试复杂交互的神器。7.3 性能优化建议复用 BrowserContext 和 Page创建浏览器实例和上下文的开销很大。如果测试框架支持如JUnit/TestNG在BeforeAll中创建一次Playwright、Browser甚至BrowserContext然后在每个Test方法中创建新的Page。测试结束后再统一清理。对于API测试的APIRequestContext如果测试用例间无状态依赖也可以考虑复用。拦截策略优化不要盲目拦截所有请求。精确指定URL模式。对于大量的静态资源如图片、样式表如果它们不影响测试逻辑最好的优化是不拦截让浏览器正常缓存和加载。或者你可以使用route.abort(“aborted”)来中止这些请求模拟它们加载失败如果这符合你的测试场景。并行执行Playwright Test runner如果你使用它原生支持测试并行化。确保你的测试是独立的无共享状态并合理配置workers数量可以极大缩短测试套件的总执行时间。选择性启动浏览器如果你的测试用例混合了纯API测试和UI测试可以将它们分开。纯API测试根本不需要启动浏览器直接使用APIRequestContext即可速度极快。网络拦截和API测试是Playwright for Java进阶之路上必须掌握的技能。它们将你的自动化测试从“表面点击”带入到“数据流验证”的深水区让你能构建更健壮、更快速、更容易定位问题的测试体系。从修改一个请求头开始到Mock一个完整的后端服务响应再到编写独立的接口测试套件每一步都在提升你对应用质量的控制力。多动手实践多结合真实项目中的场景你会逐渐发现这些高级功能带来的巨大回报。