使用Playwright进行API测试:拦截与模拟网络请求

举报
霍格沃兹测试开发学社 发表于 2026/01/08 17:24:24 2026/01/08
【摘要】 你可能已经熟悉用Playwright做端到端的UI测试,但它的能力远不止于此。在实际项目中,前后端分离的架构让我们不得不面对一个现实:UI测试虽然直观,但往往脆弱且执行缓慢。而直接测试API,特别是能够控制网络请求的流向,才是提升测试效率的关键。想象这些场景:前端页面依赖的后端接口尚未开发完成;第三方服务有调用频率限制或产生费用;某些边缘情况在生产环境中难以触发。在这些情况下,拦截和模拟网络...
你可能已经熟悉用Playwright做端到端的UI测试,但它的能力远不止于此。在实际项目中,前后端分离的架构让我们不得不面对一个现实:UI测试虽然直观,但往往脆弱且执行缓慢。而直接测试API,特别是能够控制网络请求的流向,才是提升测试效率的关键。

想象这些场景:前端页面依赖的后端接口尚未开发完成;第三方服务有调用频率限制或产生费用;某些边缘情况在生产环境中难以触发。在这些情况下,拦截和模拟网络请求就成了我们测试工具箱中的利器。

第一部分:理解Playwright的网络层

1.1 不只是浏览器自动化工具

很多人误以为Playwright只能操作浏览器,实际上它提供了完整的网络请求控制能力。每个浏览器上下文(browser context)都有自己的网络栈,这意味着你可以:

  • 监听所有进出请求
  • 修改请求参数和头信息
  • 拦截请求并返回自定义响应
  • 模拟网络条件和延迟

1.2 核心概念:Route与Fetch API

Playwright通过两个主要机制处理网络请求:

路由(Route)机制:在请求到达服务器之前拦截并处理Fetch API:直接从测试代码发起HTTP请求,不经过浏览器界面

// 路由拦截的基本模式
await page.route('**/api/users/*'async route => {
  // 在这里决定如何处理这个请求
  // 可以继续、中止或提供模拟响应
});

第二部分:实战拦截技术

2.1 基础拦截:修改请求与响应

让我们从一个实际例子开始。假设我们正在测试一个用户管理系统,需要验证前端是否正确处理API返回的数据。

import { test, expect } from'@playwright/test';

test('拦截用户列表API并验证数据处理'async ({ page }) => {
// 监听特定的API端点
await page.route('**/api/users?page=1'async route => {
    // 获取原始请求信息
    const request = route.request();
    console.log(`拦截到请求: ${request.method()} ${request.url()}`);
    
    // 检查请求头
    const authHeader = request.headerValue('Authorization');
    expect(authHeader).toContain('Bearer');
    
    // 提供模拟响应
    const mockResponse = {
      data: [
        { id: 1, name: '张三', email: 'zhangsan@example.com', status: 'active' },
        { id: 2, name: '李四', email: 'lisi@example.com', status: 'inactive' },
        { id: 3, name: '王五', email: 'wangwu@example.com', status: 'active' }
      ],
      total: 3,
      page: 1,
      per_page: 20
    };
    
    // 返回模拟响应
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify(mockResponse)
    });
  });

// 导航到页面,触发API调用
await page.goto('/user-management');

// 验证前端是否正确显示模拟数据
await expect(page.locator('.user-list-item')).toHaveCount(3);
await expect(page.locator('text=李四')).toBeVisible();
await expect(page.locator('text=inactive')).toHaveClass(/status-inactive/);
});

2.2 条件拦截:基于请求内容的动态处理

不是所有请求都需要拦截,也不是所有拦截都需要相同的处理逻辑。我们可以根据请求内容决定如何响应。

test('根据请求体内容动态拦截登录请求'async ({ page }) => {
await page.route('**/api/auth/login'async route => {
    const request = route.request();
    const postData = request.postData();
    
    if (!postData) {
      // 没有POST数据,继续原始请求
      return route.continue();
    }
    
    const credentials = JSON.parse(postData);
    
    // 根据不同测试用例模拟不同响应
    if (credentials.username === 'locked_user') {
      // 模拟账户被锁定
      await route.fulfill({
        status: 423,
        body: JSON.stringify({ 
          error: '账户已被锁定,请联系管理员'
        })
      });
    } elseif (credentials.username === 'expired_password') {
      // 模拟密码过期
      await route.fulfill({
        status: 200,
        body: JSON.stringify({ 
          token: 'temp_token',
          requires_password_change: true
        })
      });
    } else {
      // 其他情况继续原始请求
      await route.continue();
    }
  });

// 测试不同登录场景
await page.goto('/login');

// 测试锁定账户场景
await page.fill('#username''locked_user');
await page.fill('#password''anypassword');
await page.click('button[type="submit"]');
await expect(page.locator('.error-message'))
    .toContainText('账户已被锁定');

// 测试密码过期场景
await page.fill('#username''expired_password');
await page.fill('#password''oldpassword');
await page.click('button[type="submit"]');
await expect(page.locator('.password-change-prompt'))
    .toBeVisible();
});

第三部分:高级模拟技巧

3.1 创建可重用的模拟工具

在实际项目中,我们需要创建可维护的模拟工具。下面是一个实用的模拟工厂实现:

// utils/api-mocks.ts
exportclass ApiMockBuilder {
private routes: Array<{
    urlPattern: string | RegExp;
    handler: Function;
  }> = [];

// 注册模拟规则
  register(urlPattern: string | RegExp, handler: Function) {
    this.routes.push({ urlPattern, handler });
    returnthis;
  }

// 应用到页面
async applyToPage(page) {
    for (const route of this.routes) {
      await page.route(route.urlPattern, async routeInstance => {
        await route.handler(routeInstance);
      });
    }
  }

// 常用模拟的快捷方法
static createUserMocks() {
    returnnew ApiMockBuilder()
      .register('**/api/users'async route => {
        const request = route.request();
        
        if (request.method() === 'GET') {
          // 模拟获取用户列表
          await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
              data: [
                { id: 1, name: '测试用户1', role: 'admin' },
                { id: 2, name: '测试用户2', role: 'user' }
              ],
              total: 2
            })
          });
        } elseif (request.method() === 'POST') {
          // 模拟创建用户
          await route.fulfill({
            status: 201,
            headers: { 'Location''/api/users/999' },
            body: JSON.stringify({
              id: 999,
              name: '新创建的用户',
              role: 'user'
            })
          });
        }
      })
      .register('**/api/users/*'async route => {
        const request = route.request();
        const userId = request.url().match(/\/(\d+)$/)?.[1];
        
        if (request.method() === 'DELETE') {
          // 模拟删除用户
          await route.fulfill({
            status: 204
          });
        }
      });
  }
}

// 在测试中使用
test('使用模拟构建器测试用户管理'async ({ page }) => {
const mocks = ApiMockBuilder.createUserMocks();
await mocks.applyToPage(page);

await page.goto('/users');
// ... 测试逻辑
});

3.2 部分模拟:混合真实与模拟数据

有时候我们不需要完全模拟API,只需要修改部分响应或添加额外数据。

test('部分修改真实API响应'async ({ page }) => {
await page.route('**/api/products/*'async route => {
    // 先获取真实响应
    const response = await route.fetch();
    const originalBody = await response.json();
    
    // 修改特定字段用于测试
    if (originalBody.price > 100) {
      originalBody.discount_applied = true;
      originalBody.final_price = originalBody.price * 0.8;
      
      // 添加测试专用标记
      originalBody._test_note = '已应用测试折扣';
    }
    
    // 返回修改后的响应
    await route.fulfill({
      response,
      body: JSON.stringify(originalBody)
    });
  });

await page.goto('/product/123');

// 验证修改后的数据在前端的表现
const finalPrice = await page.locator('.final-price').textContent();
  expect(parseFloat(finalPrice)).toBeLessThan(100);
});

3.3 延迟和超时模拟

测试加载状态和超时处理是UI测试的重要部分。

test('模拟API延迟和超时场景'async ({ page }) => {
// 模拟慢速响应
await page.route('**/api/analytics/report'async route => {
    // 延迟3秒响应,测试加载状态
    await page.waitForTimeout(3000);
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ data: [/* 大量数据 */] })
    });
  });

// 模拟超时
await page.route('**/api/external-service/*'async route => {
    // 模拟永远不会响应的请求
    // 在实际测试中,我们可能设置一个超时后放弃
    // 这里我们直接中止请求,模拟超时
    await route.abort('timedout');
  });

await page.goto('/dashboard');

// 验证加载状态
await expect(page.locator('.loading-spinner')).toBeVisible();
await expect(page.locator('.loading-spinner')).toBeHidden({ timeout: 5000 });

// 验证超时错误处理
await page.click('#fetch-external-data');
await expect(page.locator('.error-toast'))
    .toContainText('请求超时');
});

第四部分:集成测试策略

4.1 API与UI测试的完美结合

真正的力量在于将API模拟与UI操作结合起来,创建既可靠又快速的集成测试。

test('完整的用户注册流程测试'async ({ page, request }) => {
// 模拟验证码API
let capturedEmail = '';

await page.route('**/api/send-verification'async route => {
    const requestData = JSON.parse(route.request().postData() || '{}');
    capturedEmail = requestData.email;
    
    // 在实际项目中,这里可以存储验证码供后续使用
    const testVerificationCode = '123456';
    global.testVerificationCode = testVerificationCode;
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ success: true })
    });
  });

// 模拟注册API
await page.route('**/api/register'async route => {
    const requestData = JSON.parse(route.request().postData() || '{}');
    
    // 验证业务逻辑
    if (requestData.verification_code !== global.testVerificationCode) {
      await route.fulfill({
        status: 400,
        body: JSON.stringify({ error: '验证码错误' })
      });
      return;
    }
    
    // 模拟成功注册
    await route.fulfill({
      status: 201,
      body: JSON.stringify({
        user_id: 1001,
        email: capturedEmail,
        access_token: 'mock_jwt_token_here'
      })
    });
  });

// 使用Playwright的Request API直接测试后端逻辑
// 这不是模拟,而是真实的API调用
const response = await request.post('/api/send-verification', {
    data: { email: 'test@example.com' }
  });
  expect(response.ok()).toBeTruthy();

// 进行UI测试
await page.goto('/register');
await page.fill('#email''test@example.com');
await page.click('#send-code');

// 验证UI状态
await expect(page.locator('.verification-code-input')).toBeVisible();

// 填写验证码并注册
await page.fill('#verification-code', global.testVerificationCode);
await page.fill('#password''SecurePass123!');
await page.click('#register-button');

// 验证注册成功
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome-message'))
    .toContainText('test@example.com');
});

4.2 测试文件上传和下载

文件处理是API测试中的常见需求。

test('模拟文件上传和下载API'async ({ page }) => {
// 模拟文件上传
await page.route('**/api/upload'async route => {
    const request = route.request();
    
    // 验证上传的文件信息
    const postData = request.postDataBuffer();
    // 这里可以验证文件内容
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify({
        file_id: 'mock_file_123',
        url: 'https://mock-cdn.example.com/files/mock.pdf',
        size: 1024 * 1024// 1MB
      })
    });
  });

// 模拟文件下载
await page.route('**/api/download/*'async route => {
    // 创建模拟的PDF文件
    const mockPdfBuffer = Buffer.from('%PDF-1.4 mock pdf content...');
    
    await route.fulfill({
      status: 200,
      contentType: 'application/pdf',
      headers: {
        'Content-Disposition''attachment; filename="document.pdf"'
      },
      body: mockPdfBuffer
    });
  });

await page.goto('/documents');

// 测试文件上传
const filePath = 'test-data/sample.pdf';
await page.setInputFiles('input[type="file"]', filePath);
await expect(page.locator('.upload-success')).toBeVisible();

// 测试文件下载
const downloadPromise = page.waitForEvent('download');
await page.click('text=下载文档');
const download = await downloadPromise;

// 验证下载的文件
  expect(download.suggestedFilename()).toBe('document.pdf');
});

第五部分:最佳实践与调试技巧

5.1 组织模拟代码的建议

  1. 按功能模块组织模拟:将相关的API模拟放在一起
  2. 创建模拟数据的工厂函数:避免硬编码测试数据
  3. 使用环境变量控制模拟行为:便于在不同环境切换
// mocks/auth-mocks.ts
exportconst createAuthMocks = (options = {}) => {
const defaults = {
    enable2FA: false,
    accountLocked: false,
    passwordExpired: false
  };

const config = { ...defaults, ...options };

returnasync (page) => {
    await page.route('**/api/auth/login'async route => {
      // 根据配置返回不同的模拟响应
      if (config.accountLocked) {
        return route.fulfill({ status: 423/* ... */ });
      }
      // ... 其他条件
    });
  };
};

// 在测试中使用
test('测试双重认证流程'async ({ page }) => {
const authMocks = createAuthMocks({ enable2FA: true });
await authMocks(page);
// ... 测试逻辑
});

5.2 调试网络请求

当模拟不按预期工作时,需要有效的调试手段:

test('调试API请求与响应'async ({ page }) => {
// 监听所有网络请求,记录到控制台
  page.on('request'request => {
    console.log(`>> ${request.method()} ${request.url()}`);
  });

  page.on('response'response => {
    console.log(`<< ${response.status()} ${response.url()}`);
    
    // 对于特定API,记录响应体(小心处理大响应)
    if (response.url().includes('/api/')) {
      response.text().then(text => {
        try {
          const json = JSON.parse(text);
          console.log('响应体:'JSON.stringify(json, null2).substring(0500));
        } catch {
          console.log('响应体:', text.substring(0500));
        }
      });
    }
  });

// 或者使用Playwright的调试工具
await page.route('**/*'async route => {
    const request = route.request();
    
    console.log('请求头:', request.headers());
    console.log('请求方法:', request.method());
    
    if (request.postData()) {
      console.log('POST数据:', request.postData());
    }
    
    // 继续原始请求
    await route.continue();
  });

await page.goto('/your-page');
});

选择合适的测试策略

拦截和模拟网络请求是强大的技术,但就像所有工具一样,需要明智地使用。以下是一些指导原则:

  1. 优先测试真实API:模拟是为了解决特定问题,不是替代真实的集成测试
  2. 保持模拟简单:过度复杂的模拟可能掩盖真实问题
  3. 定期验证模拟:确保模拟行为与真实API保持一致
  4. 与团队共享模拟:创建团队共享的模拟库,保持一致性

记住,最好的测试策略是分层的:单元测试确保组件逻辑正确,API模拟测试确保前端处理逻辑正确,而完整的端到端测试确保整个系统协同工作。

通过掌握Playwright的网络拦截和模拟能力,你不仅能够创建更快速、更可靠的测试,还能在前后端并行开发时保持高效。这才是现代Web应用测试应有的样子——智能、灵活且强大。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。