Playwright 网络 Mock #
网络 Mock 概述 #
Playwright 提供强大的网络拦截能力,可以拦截、修改、模拟 HTTP 请求和响应,非常适合测试各种网络场景。
应用场景 #
text
┌─────────────────────────────────────────────────────────────┐
│ 网络 Mock 应用场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ API Mock - 模拟后端响应,无需真实服务器 │
│ ✅ 错误测试 - 模拟网络错误、超时、服务器错误 │
│ ✅ 性能测试 - 模拟慢速网络、延迟响应 │
│ ✅ 安全测试 - 模拟各种安全场景 │
│ ✅ 数据隔离 - 使用固定测试数据 │
│ │
└─────────────────────────────────────────────────────────────┘
基本请求拦截 #
使用 route 拦截 #
typescript
import { test, expect } from '@playwright/test';
test('基本请求拦截', async ({ page }) => {
// 拦截所有请求
await page.route('**', route => {
console.log(`请求: ${route.request().url()}`);
route.continue();
});
// 拦截特定 URL
await page.route('**/api/users', route => {
route.continue();
});
// 拦截特定模式
await page.route('**/*.png', route => {
route.abort(); // 阻止图片加载
});
await page.goto('/');
});
route.continue #
typescript
test('继续请求', async ({ page }) => {
await page.route('**/api/**', async route => {
// 可以修改请求后继续
await route.continue({
headers: {
...route.request().headers(),
'Authorization': 'Bearer test-token',
},
});
});
await page.goto('/');
});
route.fulfill #
typescript
test('模拟响应', async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]),
});
});
await page.goto('/users');
await expect(page.locator('.user')).toHaveCount(2);
});
route.abort #
typescript
test('阻止请求', async ({ page }) => {
// 阻止所有图片请求
await page.route('**/*.{png,jpg,jpeg,gif,svg}', route => {
route.abort();
});
// 阻止特定域名
await page.route('**/analytics/**', route => {
route.abort();
});
// 阻止特定资源类型
await page.route('**', route => {
if (route.request().resourceType() === 'image') {
route.abort();
} else {
route.continue();
}
});
await page.goto('/');
});
模拟 API 响应 #
模拟 JSON 响应 #
typescript
test('模拟 JSON 响应', async ({ page }) => {
// 模拟用户列表
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
users: [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' },
],
total: 2,
}),
});
});
await page.goto('/users');
await expect(page.locator('.user-name').first()).toHaveText('John');
});
模拟错误响应 #
typescript
test('模拟错误响应', async ({ page }) => {
// 模拟 404 错误
await page.route('**/api/users/999', route => {
route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({
error: 'User not found',
}),
});
});
// 模拟 500 错误
await page.route('**/api/error', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: 'Internal server error',
}),
});
});
// 模拟 401 未授权
await page.route('**/api/protected', route => {
route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({
error: 'Unauthorized',
}),
});
});
await page.goto('/');
});
模拟延迟响应 #
typescript
test('模拟延迟响应', async ({ page }) => {
await page.route('**/api/users', async route => {
// 延迟 2 秒后响应
await new Promise(resolve => setTimeout(resolve, 2000));
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ users: [] }),
});
});
await page.goto('/users');
// 验证加载状态
await expect(page.locator('.loading')).toBeVisible();
await expect(page.locator('.loading')).not.toBeVisible({ timeout: 3000 });
});
修改请求 #
添加请求头 #
typescript
test('添加请求头', async ({ page }) => {
await page.route('**/api/**', async route => {
const headers = {
...route.request().headers(),
'Authorization': 'Bearer test-token',
'X-Custom-Header': 'custom-value',
};
await route.continue({ headers });
});
await page.goto('/');
});
修改请求体 #
typescript
test('修改请求体', async ({ page }) => {
await page.route('**/api/users', async route => {
const request = route.request();
if (request.method() === 'POST') {
const postData = request.postDataJSON();
// 修改请求数据
const modifiedData = {
...postData,
timestamp: Date.now(),
};
await route.continue({
postData: JSON.stringify(modifiedData),
});
} else {
await route.continue();
}
});
await page.goto('/');
});
修改 URL #
typescript
test('修改 URL', async ({ page }) => {
// 重定向请求到不同的服务器
await page.route('**/api/**', async route => {
const url = route.request().url();
const newUrl = url.replace('api.example.com', 'api-staging.example.com');
await route.continue({ url: newUrl });
});
await page.goto('/');
});
修改响应 #
修改响应内容 #
typescript
test('修改响应内容', async ({ page }) => {
await page.route('**/api/users', async route => {
// 获取原始响应
const response = await route.fetch();
const json = await response.json();
// 修改响应数据
json.users = json.users.map(user => ({
...user,
name: user.name.toUpperCase(),
}));
route.fulfill({
status: response.status(),
headers: response.headers(),
body: JSON.stringify(json),
});
});
await page.goto('/users');
});
修改响应头 #
typescript
test('修改响应头', async ({ page }) => {
await page.route('**', async route => {
const response = await route.fetch();
route.fulfill({
status: response.status(),
headers: {
...response.headers(),
'X-Custom-Header': 'custom-value',
'Cache-Control': 'no-cache',
},
body: await response.text(),
});
});
await page.goto('/');
});
高级 Mock 场景 #
条件 Mock #
typescript
test('条件 Mock', async ({ page }) => {
await page.route('**/api/users', route => {
const request = route.request();
// 根据请求方法返回不同响应
switch (request.method()) {
case 'GET':
route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'John' }]),
});
break;
case 'POST':
route.fulfill({
status: 201,
body: JSON.stringify({ id: 2, name: 'New User' }),
});
break;
default:
route.continue();
}
});
await page.goto('/');
});
动态 Mock #
typescript
test('动态 Mock', async ({ page }) => {
let callCount = 0;
await page.route('**/api/users', route => {
callCount++;
// 第一次返回空列表,第二次返回有数据
if (callCount === 1) {
route.fulfill({
status: 200,
body: JSON.stringify([]),
});
} else {
route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'John' }]),
});
}
});
await page.goto('/users');
await expect(page.locator('.empty-state')).toBeVisible();
await page.click('button:has-text("Refresh")');
await expect(page.locator('.user')).toHaveCount(1);
});
基于 URL 参数 Mock #
typescript
test('基于 URL 参数 Mock', async ({ page }) => {
await page.route('**/api/users**', route => {
const url = new URL(route.request().url());
const page = url.searchParams.get('page') || '1';
const limit = url.searchParams.get('limit') || '10';
route.fulfill({
status: 200,
body: JSON.stringify({
users: Array.from({ length: parseInt(limit) }, (_, i) => ({
id: (parseInt(page) - 1) * parseInt(limit) + i + 1,
name: `User ${(parseInt(page) - 1) * parseInt(limit) + i + 1}`,
})),
page: parseInt(page),
total: 100,
}),
});
});
await page.goto('/users?page=2&limit=5');
await expect(page.locator('.user')).toHaveCount(5);
});
网络故障模拟 #
模拟网络错误 #
typescript
test('模拟网络错误', async ({ page }) => {
// 模拟连接失败
await page.route('**/api/users', route => {
route.abort('failed');
});
// 模拟超时
await page.route('**/api/slow', route => {
route.abort('timedout');
});
// 模拟 DNS 解析失败
await page.route('**/api/unknown', route => {
route.abort('namenotresolved');
});
await page.goto('/');
});
模拟慢速网络 #
typescript
test('模拟慢速网络', async ({ page }) => {
await page.route('**', async route => {
// 延迟每个请求
await new Promise(resolve => setTimeout(resolve, 500));
await route.continue();
});
await page.goto('/');
});
模拟离线状态 #
typescript
test('模拟离线状态', async ({ page, context }) => {
await context.setOffline(true);
await page.goto('/');
// 验证离线提示
await expect(page.locator('.offline-message')).toBeVisible();
// 恢复在线
await context.setOffline(false);
});
Mock 最佳实践 #
创建 Mock 辅助函数 #
typescript
// utils/mock.ts
export async function mockUsersApi(page: Page, users: User[]) {
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(users),
});
});
}
export async function mockProductsApi(page: Page, products: Product[]) {
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(products),
});
});
}
// 使用
test('用户列表测试', async ({ page }) => {
await mockUsersApi(page, [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]);
await page.goto('/users');
});
使用 Fixtures 管理 Mock #
typescript
// fixtures/mock.ts
import { test as base } from '@playwright/test';
type MockFixtures = {
mockApi: {
users: (users: User[]) => Promise<void>;
products: (products: Product[]) => Promise<void>;
error: (endpoint: string, status: number) => Promise<void>;
};
};
export const test = base.extend<MockFixtures>({
mockApi: async ({ page }, use) => {
const mockApi = {
users: async (users: User[]) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
body: JSON.stringify(users),
});
});
},
products: async (products: Product[]) => {
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
body: JSON.stringify(products),
});
});
},
error: async (endpoint: string, status: number) => {
await page.route(`**/api/${endpoint}`, route => {
route.fulfill({
status,
body: JSON.stringify({ error: `Error ${status}` }),
});
});
},
};
await use(mockApi);
// 清理所有路由
await page.unrouteAll();
},
});
从文件加载 Mock 数据 #
typescript
// fixtures/mock-data.ts
import { test as base } from '@playwright/test';
import fs from 'fs/promises';
type MockDataFixtures = {
loadMockData: (filename: string) => Promise<any>;
};
export const test = base.extend<MockDataFixtures>({
loadMockData: async ({}, use) => {
const loadMockData = async (filename: string) => {
const content = await fs.readFile(`mocks/${filename}`, 'utf-8');
return JSON.parse(content);
};
await use(loadMockData);
},
});
// 使用
test('使用文件数据 Mock', async ({ page, loadMockData }) => {
const users = await loadMockData('users.json');
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
body: JSON.stringify(users),
});
});
await page.goto('/users');
});
调试 Mock #
记录请求 #
typescript
test('记录请求', async ({ page }) => {
const requests: any[] = [];
page.on('request', request => {
requests.push({
url: request.url(),
method: request.method(),
headers: request.headers(),
postData: request.postData(),
});
});
await page.goto('/');
console.log('所有请求:', requests);
});
验证 Mock 调用 #
typescript
test('验证 Mock 调用', async ({ page }) => {
let apiCallCount = 0;
await page.route('**/api/users', route => {
apiCallCount++;
route.fulfill({
status: 200,
body: JSON.stringify([]),
});
});
await page.goto('/users');
expect(apiCallCount).toBe(1);
});
下一步 #
现在你已经掌握了网络 Mock,接下来学习 高级配置 了解更多 Playwright 配置选项!
最后更新:2026-03-28