Playwright 第10章:网络拦截与 Mock
网络拦截是 Playwright 的强大功能之一。通过 page.route() 可以拦截、修改和模拟网络请求,实现 Mock API 响应、修改请求头、拦截资源加载等功能,使测试更加可控和高效。
page.route() 基础
请求拦截
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
// 拦截所有请求
await page.route('**/*', (route) => {
console.log('请求:', route.request().url());
route.continue(); // 继续请求
});
// 拦截特定 URL 模式
await page.route('**/api/**', (route) => {
console.log('API 请求:', route.request().url());
route.continue();
});
await page.goto('https://example.com');
await browser.close();
路由拦截匹配规则
const page = await browser.newPage();
// 字符串匹配
await page.route('https://example.com/api/data', handler);
// 通配符匹配
await page.route('**/api/**', handler);
await page.route('*/api/users', handler);
await page.route('https://example.com/**', handler);
// 正则匹配
await page.route(/api\/users\/\d+/, handler);
await page.route(/\.json$/, handler);
// 函数匹配
await page.route((url) => {
return url.pathname.startsWith('/api') && url.searchParams.has('token');
}, handler);
// 同时拦截多个域名
await page.route(/example\.com|test\.com/, handler);
Mock API 响应
返回模拟数据
const page = await browser.newPage();
// Mock GET 请求
await page.route('**/api/users', async (route) => {
const json = {
users: [
{ id: 1, name: '张三', email: '[email protected]' },
{ id: 2, name: '李四', email: '[email protected]' },
{ id: 3, name: '王五', email: '[email protected]' },
],
total: 3,
};
await route.fulfill({
status: 200,
contentType: 'application/json',
headers: { 'X-Custom': 'mock' },
body: JSON.stringify(json),
});
});
// Mock POST 请求
await page.route('**/api/login', async (route) => {
const request = route.request();
const postData = request.postDataJSON();
if (postData.username === 'admin' && postData.password === 'pass123') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
token: 'mock-token-abc123',
user: { id: 1, name: 'Admin' },
}),
});
} else {
await route.fulfill({
status: 401,
contentType: 'application/json',
body: JSON.stringify({
error: '用户名或密码错误',
}),
});
}
});
await page.goto('https://example.com');
返回错误状态
const page = await browser.newPage();
// Mock 500 错误
await page.route('**/api/data', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: '服务器内部错误',
code: 'INTERNAL_ERROR',
}),
});
});
// Mock 超时
await page.route('**/api/slow', async (route) => {
await new Promise(resolve => setTimeout(resolve, 30000));
await route.fulfill({ status: 504 });
});
// Mock 404
await page.route('**/api/notfound', async (route) => {
await route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({ error: '资源不存在' }),
});
});
// 模拟网络错误
await page.route('**/api/fail', (route) => {
route.abort('connectionrefused');
});
修改请求
修改请求头和参数
const page = await browser.newPage();
// 修改请求头
await page.route('**/api/**', async (route) => {
const headers = {
...route.request().headers(),
'Authorization': 'Bearer mock-token',
'X-Testing': 'true',
};
await route.continue({ headers });
});
// 修改请求体
await page.route('**/api/submit', async (route) => {
const postData = route.request().postDataJSON();
// 修改请求数据
postData.timestamp = Date.now();
postData.source = 'test';
await route.continue({
postData: JSON.stringify(postData),
});
});
// 修改 URL
await page.route('**/api/data', async (route) => {
const url = new URL(route.request().url());
url.searchParams.set('mock', 'true');
url.searchParams.set('lang', 'zh-CN');
await route.continue({
url: url.toString(),
});
});
资源拦截
拦截图片/样式/脚本
const page = await browser.newPage();
// 拦截所有图片以加速测试
await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', (route) => {
route.abort();
});
// 拦截 Google Analytics
await page.route('**/analytics.js', (route) => {
route.abort();
});
// 拦截字体文件
await page.route('**/*.{woff,woff2,ttf,eot}', (route) => {
route.abort();
});
// 拦截特定 CDN 的样式
await page.route('**/cdn.example.com/**', (route) => {
route.abort();
});
// 有条件地拦截
await page.route('**/*.css', (route) => {
const url = route.request().url();
if (url.includes('bootstrap')) {
route.abort(); // 不加载 Bootstrap
} else {
route.continue();
}
});
修改响应内容
const page = await browser.newPage();
// 修改 HTML 响应
await page.route('**/index.html', async (route) => {
const response = await route.fetch();
let body = await response.text();
// 替换页面内容
body = body.replace(
'<title>Example</title>',
'<title>测试页面</title>'
);
body = body.replace(
'</body>',
'<div class="test-banner">测试环境</div></body>'
);
await route.fulfill({
response,
body,
});
});
// 修改 JSON 响应
await page.route('**/api/config', async (route) => {
const response = await route.fetch();
const json = await response.json();
// 修改配置
json.features.newFeature = true;
json.debug = true;
json.timeout = 0; // 禁用超时
await route.fulfill({
response,
body: JSON.stringify(json),
});
});
离线测试
模拟离线状态
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// 模拟离线
await context.setOffline(true);
// 测试离线页面
await page.goto('https://example.com').catch(() => {});
await expect(page.locator('.offline-message')).toBeVisible();
// 恢复在线
await context.setOffline(false);
await page.reload();
await expect(page.locator('.online-content')).toBeVisible();
网络请求验证
监听和断言网络请求
import { test, expect } from '@playwright/test';
test('验证网络请求', async ({ page }) => {
// 监听特定 API
const apiPromise = page.waitForResponse(
response => response.url().includes('/api/data') && response.status() === 200
);
await page.goto('/');
await page.click('.load-data');
// 等待并验证响应
const response = await apiPromise;
const data = await response.json();
expect(data.success).toBe(true);
expect(data.items).toHaveLength(10);
});
test('验证请求参数', async ({ page }) => {
// 捕获请求
let capturedRequest: Request | null = null;
page.on('request', (request) => {
if (request.url().includes('/api/search')) {
capturedRequest = request;
}
});
await page.goto('/');
await page.fill('#search', 'Playwright');
await page.click('.search-btn');
// 验证请求参数
expect(capturedRequest).toBeTruthy();
const postData = capturedRequest!.postDataJSON();
expect(postData.query).toBe('Playwright');
expect(postData.page).toBe(1);
});
完整示例:Mock 完整的 CRUD API
import { test, expect } from '@playwright/test';
test.describe('用户管理 CRUD 测试', () => {
let mockUsers = [
{ id: 1, name: '张三', email: '[email protected]' },
{ id: 2, name: '李四', email: '[email protected]' },
];
test.beforeEach(async ({ page }) => {
// Mock 用户列表接口
await page.route('**/api/users', async (route) => {
if (route.request().method() === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockUsers),
});
} else if (route.request().method() === 'POST') {
const data = route.request().postDataJSON();
const newUser = { id: mockUsers.length + 1, ...data };
mockUsers.push(newUser);
await route.fulfill({
status: 201,
body: JSON.stringify(newUser),
});
}
});
await page.goto('/users');
});
test('显示用户列表', async ({ page }) => {
await expect(page.locator('.user-row')).toHaveCount(2);
});
test('创建新用户', async ({ page }) => {
await page.click('.add-user');
await page.fill('#name', '王五');
await page.fill('#email', '[email protected]');
await page.click('.save-btn');
await expect(page.locator('.user-row')).toHaveCount(3);
});
});
总结
网络拦截是 Playwright 测试中极其强大的功能。通过 page.route() 可以模拟各种 API 响应、网络状态和错误场景,使测试不依赖后端服务即可运行。资源拦截可以加速测试运行,请求修改可以模拟认证和特殊参数。在实际项目中,建议将 Mock 数据封装为独立模块,方便在不同测试间复用。