欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > Playwright+Next.js:实例演示服务器端 API 模拟新方法

Playwright+Next.js:实例演示服务器端 API 模拟新方法

2025/6/1 6:16:52 来源:https://blog.csdn.net/deerxiaoluaa/article/details/148080879  浏览:    关键词:Playwright+Next.js:实例演示服务器端 API 模拟新方法

在现代网络开发中,端到端测试对于确保应用程序按预期运行至关重要。当使用像 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 作为传输层来解决连接问题:

  然而,这种方法也有其自身的局限性,如拉取请求中所指出的:

  > 你不能同时对同一个应用程序进行多个测试,这些测试会覆盖相同请求的处理程序。

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

  总的来说,现有方法旨在使用任意函数作为模拟请求处理程序,但引入了连接和并行化的挑战。

  提出的解决方案

  在尝试这些解决方案时,我得出了一个更简单的想法:

  如果我们通过自定义 HTTP 标头在导航请求中传递模拟数据会怎样?

  它的工作原理

  - 嵌入模拟数据:而不是将服务器端请求通过外部代理路由,我们将静态模拟响应编码为 JSON 并附加到自定义标头(例如 `x-mock-request`)中。

  - 服务器端解析:在服务器端,我们拦截传出的 API 调用,读取自定义标头,并在请求匹配预定义模式时应用相应的模拟。

  这种方法解决了连接和并行化的问题:

  - 无需设置隧道或启动单独的代理服务器。

  - 每个测试都可以通过 HTTP 标头传递自己的模拟数据,而不会发生冲突。

        可以到我的个人号:atstudy-js,这里有10W+ 热情踊跃的测试小伙伴们,一起交流行业热点、测试技术各种干货,一起共享面试经验、跳槽求职各种好用的。

         多行业测试学习交流群,内含直播课+实战+面试资料

        AI测试、 车载测试、自动化测试、银行、金融、游戏、AIGC.

  当然,这种方法也有局限性:

  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.jsexport 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 testRunning 1 test using 1 worker1 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.onChange = async (headers) => context.setExtraHTTPHeaders(headers);await use(mockClient);},});

  在服务器端,你可以通过调用 `setupFetchInterceptor()` 来设置拦截器:  

// instrumentation.jsimport { 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 标头有大小限制,这使得该方法最适合较小的负载。

  尽管存在这些权衡,但该解决方案看起来很有希望。我已将其封装到一个库中,以便更容易地与不同框架集成。欢迎你尝试并分享反馈。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词