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