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

B站影视 日本电影 2025-05-13 01:30 2

摘要:在现代网络开发中,端到端测试对于确保应用程序按预期运行至关重要。当使用像 React Server Components 这样的新功能时,我们通常依赖第三方 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 ( {users.map((user) => ( {user.name} ))} );}

当浏览器请求此页面时,服务器会向 `/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 标头传递自己的模拟数据,而不会发生冲突。

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

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

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

AI测试涨薪交流群,内含银行业务、车载、互联网、游戏更多行业测试实战和面试题库 &【软件测试专用AI提示词包】等各种好用的

来源:啊将登胡

相关推荐