ITPub博客

首页 > 人工智能 > 人工智能 > Playwright+Next.js:快速且可靠的服务器端API模拟新方法

Playwright+Next.js:快速且可靠的服务器端API模拟新方法

原创 人工智能 作者:Atstudy技术社区 时间:2025-05-20 09:55:57 0 删除 编辑

在现代网络开发中,端到端 测试对于确保应用程序按预期运行至关重要。当使用像 React Server Components 这样的新功能时,我们通常依赖第三方 API 在服务器端获取数据。虽然这种方法在性能和可扩展性方面提供了显著优势,但它也为测试带来了挑战。实时 API 响应可能会随着时间的推移而变化,即使应用程序逻辑仍然正确,也可能导致测试失败。

  在本文中,我将介绍一种新的服务器端 API 模拟方法,这种方法可以让测试快速且可靠,且设置工作量最小。在技术栈方面,我将使用 Playwright和Next.js,尽管这种方法适用于任何框架或测试运行器。让我们开始吧!
   问题:测试服务器端数据获取
  考虑以下服务器组件,它从第三方 API 获取并渲染用户列表:
  export async function UserList() {
    const res = await fetch('https://jsonplaceholder.typicode.com/users');
    const users = await res.json();
    return (
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    );
  }
  当浏览器请求此页面时,服务器会向 `/users` API 发起后续调用并返回渲染后的 HTML:
  浏览器中的页面:
  一个基本的 Playwright 测试可能如下所示:
  test('show user list', async ({ page }) => {
    await page.goto('/');
    await expect(page.getByRole('listitem').first()).toHaveText('Leanne Graham');
  });
  如果 API 响应中的第一个条目是 Leanne Graham,此测试将通过。但如果 API 返回不同的顺序或数据,即使应用程序本身运行正常,测试也会失败。
  当元素以相反的顺序返回时,测试失败的示例:
  如果测试能够模拟 `GET /users` 请求并提供一个静态的用户列表,测试将更加可靠:
  当从浏览器上下文中发起请求时,Playwright 的 API 模拟功能可以正常工作。然而,这种方法无法拦截服务器端请求。
   现有方法
  人们已经尝试了多种方法来解决这一挑战:
   Playwright 代理方法
  Playwright 仓库中正在进行的拉取请求通过运行一个与测试进程并行的 HTTP 代理来引入服务器端模拟。应用程序被配置为将传出请求通过此代理路由。如果请求匹配指定的 URL 模式,用户定义的处理程序将应用模拟响应。
  尽管这种方法非常灵活,但它也带来了挑战。例如,如果你的应用程序部署在像 Vercel 这样的平台上,测试在 GitHub 工作流中运行,那么设置隧道以连接应用程序和代理可能会非常复杂且容易出错。
   Mock Service Worker (MSW)
  MSW 是一个流行的用于模拟 HTTP 请求的工具。它也在开发服务器端模拟支持,如这个拉取请求所示。与使用 HTTP 代理不同,MSW 依赖 WebSocket 作为传输层来解决连接问题:
  然而,这种方法也有其自身的局限性,如拉取请求中所指出的:
  > 你不能同时对同一个应用程序进行多个测试,这些测试会覆盖相同请求的处理程序。

  这意味着具有服务器端模拟的测试不能并行运行,这对于端到端测试来说是一个重大缺点。

       可以到我的个人号:atstudy-js,这里有10W+ 热情踊跃的测试小伙伴们, 一起交流行业热点、测试技术各种干货, 一起共享面试经验、跳槽求职各种好用的。 欢迎加入->  多行业测试学习交流群,内含直播课+实战+面试资料。 【AI测试、 车载测试、自动化测试、银行、金融、游戏、AIGC...】

  总的来说,现有方法旨在使用任意函数作为模拟请求处理程序,但引入了连接和并行化的挑战。
   提出的解决方案
  在尝试这些解决方案时,我得出了一个更简单的想法:
  如果我们通过自定义 HTTP 标头在导航请求中传递模拟数据会怎样?
   它的工作原理
   - 嵌入模拟数据:而不是将服务器端请求通过外部代理路由,我们将静态模拟响应编码为 JSON 并附加到自定义标头(例如 `x-mock-request`)中。
   - 服务器端解析:在服务器端,我们拦截传出的 API 调用,读取自定义标头,并在请求匹配预定义模式时应用相应的模拟。
  这种方法解决了连接和并行化的问题:
  - 无需设置隧道或启动单独的代理服务器。
  - 每个测试都可以通过 HTTP 标头传递自己的模拟数据,而不会发生冲突。
  当然,这种方法也有局限性:
  1. 仅支持静态数据:模拟必须能够序列化为 JSON。这意味着你只能提供静态响应(例如 `{ status: 200, body: 'Hello' }`),而不是基于函数的动态模拟。
  2. 标头大小限制:HTTP 标头通常支持 4KB 到 8KB 的数据。这种方法最适合小负载。
  在许多实际场景中,这些限制是可以接受的。大多数模拟都是轻量级且静态的,这使得这种方法成为确保测试稳定性的实用解决方案。
   实现
  以下是使用 Playwright 和 Next.js 实现此解决方案的逐步指南。
   定义模式
  首先,定义请求和响应的模式。例如,要模拟对 `https://jsonplaceholder.typicode.com/users` 的服务器端 GET 请求,你可以设置如下:
  请求模式:
  const reqSchema = {
    method: 'GET',
    url: 'https://jsonplaceholder.typicode.com/users',
  };
  响应模式:
  const resSchema = {
    status: 200,
    body: [
      { id: 1, name: 'John Smith' }
    ]
  };
  合并模式并构建标头:
  const mockSchema = { reqSchema, resSchema };
  const mockSchemaString = JSON.stringify(mockSchema);
  const headers = {
    'x-mock-request': mockSchemaString
  };
   Playwright 集成
  要将自定义 HTTP 标头附加到导航请求中,请使用 Playwright 的 page.setExtraHTTPHeaders:
  test('show user list', async ({ page }) => {
    await page.setExtraHTTPHeaders({
      'x-mock-request': mockSchemaString
    });
    await page.goto('/');
  });
  有了此配置,页面的每次导航和后续请求都将包含模拟标头。
   在服务器端处理
  在服务器端,需要执行以下步骤:
  1. 读取传入标头
  2. 获取 `x-mock-request` 值并提取模拟模式
  3. 拦截传出请求
  4. 应用模拟模式并返回模拟响应
   读取传入标头并提取模式
  要读取传入标头,你可以使用 Next.js 的 `headers()` 辅助函数。找到 `x-mock-request` 标头时,使用 `JSON.parse()` 提取模拟模式:
  import { headers } from 'next/headers';
  // ...
  const headersList = await headers();
  const mockHeader = headersList.get('x-mock-request');
  const mockSchemas = JSON.parse(mockHeader);
   拦截传出请求
  要在 Next.js 应用程序中拦截所有传出请求,你可以覆盖 `globalThis.fetch` 函数:
  const originalFetch = globalThis.fetch;
  globalThis.fetch = async (input, init) => {
    // 检查并可能模拟传出请求
  };
  在拦截的函数中,你可以读取传入标头并应用模拟。完整函数代码如下:
  function interceptGlobalFetch() {
    const originalFetch = globalThis.fetch;
    globalThis.fetch = async (input, init) => {
      // 读取传入标头并提取模拟
      const headersList = await headers();
      const mockHeader = headersList.get('x-mock-request');
      const mockSchemas = JSON.parse(mockHeader);
      // 将请求与模式匹配
      const request = new Request(input, init);
      const matchedSchema = mockSchemas.find(schema => matchRequest(request, schema));
      // 返回模拟响应或发起真实请求
      return matchedSchema
        ? buildMockedResponse(request, matchedSchema)
        : originalFetch(request)
    };
  }
  应在服务器启动时对全局 `fetch` 进行工具化,且在发出任何请求之前。Next.js 为此任务提供了一个专用文件,名为 instrumentation.js:
  // instrumentation.js
  export async function register() {
    if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NODE_ENV !== 'production') {
      interceptGlobalFetch();
    }
  }
  > **注意**:仅应在 `nodejs` 运行时和非生产环境中启用拦截。
   测试整个流程
  一旦服务器端拦截就绪,你就可以运行带有服务器端模拟的 Playwright 测试。以下是一个示例:
  test('show user list', async ({ page }) => {
    // 设置服务器端模拟
    await page.setExtraHTTPHeaders({
      'x-mock-request': buildMockHeader()
    });
    // 导航到页面
    await page.goto('/');
    // 根据模拟数据断言页面内容
    await expect(page.getByRole('listitem').first()).toHaveText('John Smith');
  });
  `buildMockHeader()` 辅助函数只是合并请求和响应模式:
  function buildMockHeader() {
    const reqSchema = {
      method: 'GET',
      url: 'https://jsonplaceholder.typicode.com/users',
    };
    const resSchema = {
      status: 200,
      body: [
        { id: 1, name: 'John Smith' }
      ]
    };
    return JSON.stringify([ { reqSchema, resSchema } ]);
  }
  运行测试:
  > npx playwright test
  Running 1 test using 1 worker
    1 passed (1.3s)
  页面的截图显示了一个带有模拟数据的列表 - 单个用户 `John Smith`:
  有了这样的模拟,测试不再依赖 API 响应,同时确保服务器组件正确渲染数据。
   封装为库
  为了减少服务器端模拟的样板代码,我将功能封装到一个名为 request-mocking-protocol 的独立包中。它隐藏了实现细节,并为在客户端和服务器端设置模拟提供了友好的 API。
  使用库的示例
  以下示例展示了如何在 Playwright 测试中使用该库:
  test('show user list', async ({ page, mockServerRequest }) => {
    // 设置服务器端模拟
    await mockServerRequest.GET('https://jsonplaceholder.typicode.com/users', {
      body: [{ id: 1, name: 'John Smith' }],
    });
    // 导航到页面
    await page.goto('/');
    // 根据模拟数据断言页面内容
    await expect(page.getByRole('listitem').first()).toHaveText('John Smith');
  });
  自定义 fixture `mockServerRequest` 定义如下:
  import { test as base } from '@playwright/test';
  import { MockClient } from 'request-mocking-protocol';
  export const test = base.extend({
    mockServerRequest: async ({ context }, use) => {
      const mockClient = new MockClient();
      mockClient.(headers) => context.setExtraHTTPHeaders(headers);
      await use(mockClient);
    },
  });
  在服务器端,你可以通过调用 `setupFetchInterceptor()` 来设置拦截器:
  // instrumentation.js
  import { headers } from 'next/headers';
  export async function register() {
    if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.NODE_ENV !== 'production') {
      const { setupFetchInterceptor } = await import('request-mocking-protocol/fetch');
      setupFetchInterceptor(() => headers());
    }
  }
   总结
  在本文中,我介绍了一种替代的服务器端请求模拟方法,该方法使用 HTTP 标头传输模拟数据。这种设置更简单,因为它消除了对额外代理的需求。每个测试都携带自己的模拟数据,允许并行执行和更好的可扩展性。
  这种方法确实有一些局限性。它仅支持静态模拟 —— 不允许使用任意 JavaScript 函数。此外,HTTP 标头有大小限制,这使得该方法最适合较小的负载。
  尽管存在这些权衡,但该解决方案看起来很有希望。我已将其封装到一个库中,以便更容易地与不同框架集成。欢迎你尝试并分享反馈。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70034708/viewspace-3084761/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2023-10-23

  • 博文量
    99
  • 访问量
    96500