Playwright 网络拦截与 Mock
本章介绍如何使用 Playwright 拦截、修改和模拟网络请求,实现不依赖后端 API 的独立测试。
什么是网络拦截
网络拦截允许你在浏览器发起 HTTP 请求时进行干预。
你可以阻止请求、修改响应、返回模拟数据,让测试不依赖外部 API 或网络状态。
page.route() 拦截请求
page.route(url, handler) 用于拦截匹配指定 URL 模式的所有网络请求。
基本拦截
实例
test('拦截所有图片请求', async ({ page }) => {
// 拦截所有 PNG 和 JPEG 图片
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
// 导航到页面(图片不会被加载)
await page.goto('https://www.runoob.com/');
// 页面可能加载更快,因为没有下载图片
});
// 拦截所有 PNG 和 JPEG 图片
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
// 导航到页面(图片不会被加载)
await page.goto('https://www.runoob.com/');
// 页面可能加载更快,因为没有下载图片
});
URL 匹配模式支持以下格式:
| 模式 | 示例 | 说明 |
|---|---|---|
| 完整 URL | 'https://api.example.com/users' | 精确匹配 |
| Glob 通配符 | '**/api/**' | ** 匹配任意路径 |
| 正则表达式 | /\/api\/v\d\/users/ | 灵活匹配 |
| 函数 | url => url.includes('/api/') | 编程逻辑匹配 |
route.abort() 阻止请求
实例
test('阻止第三方分析脚本', async ({ page }) => {
// 阻止 Google Analytics 等分析脚本
await page.route('**/*analytics*', route => route.abort());
// 阻止 CSS 文件
await page.route('**/*.css', route => route.abort());
await page.goto('https://www.runoob.com/');
// 页面以无样式形式加载,但不影响功能测试
});
// 阻止 Google Analytics 等分析脚本
await page.route('**/*analytics*', route => route.abort());
// 阻止 CSS 文件
await page.route('**/*.css', route => route.abort());
await page.goto('https://www.runoob.com/');
// 页面以无样式形式加载,但不影响功能测试
});
route.fulfill() 模拟响应
route.fulfill() 是最强大的 Mock 方法,可以返回任意自定义响应。
Mock JSON 数据
实例
test('Mock 用户列表 API', async ({ page }) => {
// 拦截用户列表 API,返回模拟数据
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
json: [
{ id: 1, name: 'runoob_user', email: 'user@runoob.com' },
{ id: 2, name: 'test_user', email: 'test@runoob.com' },
],
});
});
await page.goto('https://www.runoob.com/users');
// 页面会显示 Mock 数据而不是真实的 API 数据
});
// 拦截用户列表 API,返回模拟数据
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
json: [
{ id: 1, name: 'runoob_user', email: 'user@runoob.com' },
{ id: 2, name: 'test_user', email: 'test@runoob.com' },
],
});
});
await page.goto('https://www.runoob.com/users');
// 页面会显示 Mock 数据而不是真实的 API 数据
});
Mock 错误响应
实例
test('测试 API 错误处理', async ({ page }) => {
// 模拟 500 服务器错误
await page.route('**/api/data', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
json: { error: '服务器内部错误' },
});
});
await page.goto('https://www.runoob.com/dashboard');
// 验证错误提示是否正常显示
await expect(page.getByText('服务器内部错误')).toBeVisible();
});
// 模拟 500 服务器错误
await page.route('**/api/data', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
json: { error: '服务器内部错误' },
});
});
await page.goto('https://www.runoob.com/dashboard');
// 验证错误提示是否正常显示
await expect(page.getByText('服务器内部错误')).toBeVisible();
});
route.continue() 修改后继续
不是直接返回模拟数据,而是修改真实请求或响应后继续发送到服务器。
实例
test('修改请求头', async ({ page }) => {
await page.route('**/api/**', async (route, request) => {
// 修改请求头,添加自定义 Header
const headers = {
...request.headers(),
'X-Custom-Header': 'RUNOOB-TEST',
};
await route.continue({ headers });
});
await page.goto('https://www.runoob.com/');
});
test('修改 POST 数据', async ({ page }) => {
await page.route('**/api/login', async (route, request) => {
// 覆盖 POST 请求的 body
const postData = JSON.parse(request.postData() || '{}');
postData.mode = 'test'; // 添加测试标记
await route.continue({
postData: JSON.stringify(postData),
});
});
});
await page.route('**/api/**', async (route, request) => {
// 修改请求头,添加自定义 Header
const headers = {
...request.headers(),
'X-Custom-Header': 'RUNOOB-TEST',
};
await route.continue({ headers });
});
await page.goto('https://www.runoob.com/');
});
test('修改 POST 数据', async ({ page }) => {
await page.route('**/api/login', async (route, request) => {
// 覆盖 POST 请求的 body
const postData = JSON.parse(request.postData() || '{}');
postData.mode = 'test'; // 添加测试标记
await route.continue({
postData: JSON.stringify(postData),
});
});
});
route.fetch() 真实请求
在 Mock 处理函数中,可以用 route.fetch() 实际发送请求并获取真实响应,然后修改。
实例
test('修改真实响应', async ({ page }) => {
await page.route('**/api/products', async route => {
// 先获取真实响应
const response = await route.fetch();
const body = await response.json();
// 修改响应数据
body.items[0].name = 'RUNOOB 修改后的商品名';
// 返回修改后的数据
await route.fulfill({
status: response.status(),
contentType: 'application/json',
json: body,
});
});
});
await page.route('**/api/products', async route => {
// 先获取真实响应
const response = await route.fetch();
const body = await response.json();
// 修改响应数据
body.items[0].name = 'RUNOOB 修改后的商品名';
// 返回修改后的数据
await route.fulfill({
status: response.status(),
contentType: 'application/json',
json: body,
});
});
});
Context 级拦截 vs Page 级拦截
| 方式 | 作用范围 | 适用场景 |
|---|---|---|
page.route() | 仅当前页面 | 单个页面的网络 Mock |
context.route() | 该 Context 下所有页面 | 多个页面共享的 Mock 规则 |
实例
test.beforeEach(async ({ context }) => {
// 在整个 Context 中拦截,所有页面生效
await context.route('**/*.css', route => route.abort());
});
test('测试 1', async ({ page }) => { /* 无 CSS */ });
test('测试 2', async ({ page }) => { /* 无 CSS */ });
// 在整个 Context 中拦截,所有页面生效
await context.route('**/*.css', route => route.abort());
});
test('测试 1', async ({ page }) => { /* 无 CSS */ });
test('测试 2', async ({ page }) => { /* 无 CSS */ });
HAR 文件录制与回放
HAR(HTTP Archive)文件可以记录页面的所有网络请求,然后回放模拟。
实例
test('使用 HAR 文件 Mock', async ({ page }) => {
// 从 HAR 文件加载网络记录并回放
await page.routeFromHAR('tests/hars/api-responses.har');
await page.goto('https://www.runoob.com/');
// 所有匹配的请求将从 HAR 文件中获取响应
});
// 从 HAR 文件加载网络记录并回放
await page.routeFromHAR('tests/hars/api-responses.har');
await page.goto('https://www.runoob.com/');
// 所有匹配的请求将从 HAR 文件中获取响应
});
# 录制 HAR 文件 npx playwright open --save-har=tests/hars/api-responses.har https://www.runoob.com/
HAR 文件适合 Mock 大量或复杂的 API 响应。先录制一次,之后测试都从 HAR 回放,不依赖真实服务器。
