使用Playwright进行API契约测试

使用Playwright进行API契约测试

· 1,821 词 · 10 分钟 读完 playwright进阶 翻译

有时作为测试工程师,业务对测试的要求可能相当奇怪,你必须在一个测试套件中采用不同类型的测试。

图片2: Midjourney提示

什么是契约测试

契约测试是一种软件测试类型,专注于验证单独组件/服务之间的交互(通常是两个微服务)。当两个微服务通过 API 交互时,一个服务以预定义的格式发送请求,另一个以预定义的格式响应。这种格式被称为"契约" - 服务(甚至开发团队)之间关于如何承诺相互通信的协议。

"契约"可以是 API 规范,但更常见的是仅仅是作为 JSON 文件的请求和响应体架构,这些文件在两个服务之间共享,它们都根据这些架构测试自己的 API - 这种方法甚至被区分为一种单独的测试方法"基于架构的契约测试"。

在客户端-服务器架构的情况下,前端可以作为各种 API 的消费者或提供者,反之亦然:

图片3: 前端应用作为契约测试的消费者或提供者

在许多文章中(参见文章末尾的链接),契约测试与集成测试或端到端测试相对立。但在本文中,我想展示契约测试可以是端到端测试的一部分 - 它可以只是特定检查的工具。

这种情况可能发生在前端自动测试的特定业务需求的情况下,例如检查你的前端是否以特定格式向第三方 API 发出特定请求。换句话说,确保 UI 发送正确的数据。

此外,这些第三方 API可能不允许在测试期间被请求。这看起来显然需要用模拟来"关闭"这些第三方 API,但如果你的测试项目上没有任何复杂的模拟基础设施(和/或不想有)怎么办?如果你的测试在实时的模拟环境中运行,可能就是这种情况。在这种情况下,你可以在网络级别通过 Playwright来"关闭"对第三方 API 的请求。

在互联网上找到类似的项目并不是问题。有很多 DeFi 初创公司使用开放 API 作为他们的基础设施,但这些 API 大多是 GraphQL 和 JSON-RPC - 这给示例增加了一点复杂性。描述它们与 REST API 的区别不是本文的主题。

怎么做契约测试

至少我找到了Sushi加密货币交换页面,其前端只向第三方 API 发出几个所需的 POST 请求(API 的 URL 与当前网站不同):

图片4: 网站的前端向第三方API发出POST请求

同样的情况在图示表示中如下所示:

图片5: 网站的前端向第三方API发出POST请求

让我提醒你,我关注第三方 API 是因为检查内部 API 不是本文的主题 - 你可以通过内部 API 测试和/或集成测试来检查你的内部 API。

在契约测试中,假定每个组件/服务都是相互隔离的。在这里,你可以使用 Playwright 的网络功能轻松地将前端与第三方 API 隔离:

  1. 粗略地中止请求 - 请求不会发送到外部 API;
  2. 或者模拟它

对于第二种情况,如果你只使用fulfill()类,你可以通过中止请求来修改响应。但如果你将fulfill()fetch()一起使用,请求将被发送到外部 API。无论哪种方式,当你用 JSON 填充响应体时 - 你就在进行契约测试(检查客户端是否正在处理填充的响应),如果这个 JSON 模式与外部 API 端用于测试的模式相同。

对于这两种情况,你通过waitForRequest()类拦截请求,以测试 POST 的请求体是否符合你的契约(当然,对于 PUT 或 PATCH 方法也是如此):

图片6: 通过Playwright拦截HTTP请求

如果你的请求体是 JSON 格式(我认为这种情况会占 90%),你可以立即使用postDataJSON()类,通过你喜欢的工具比较 JSON 模式:Ajv, Zod,或者如果由于某些原因你决定直接比较两个 JSON 对象,可以使用toEqual()断言。

当你只检查请求的契约时,你可能不需要响应,可以简单地中止它(注意,正确的行为取决于你的应用,也许你必须模拟响应以防止应用崩溃):

图片7: route.abort()在Playwright Inspector中的工作原理

这里是这样一个测试的代码示例:

import { expect, type Page, test } from "@playwright/test";
import { z } from "zod";

// 契约
const schema = z.object({
  jsonrpc: z.string(),
  id: z.number(),
  method: z.string(),
  params: z.array(z.union([z.string(), z.boolean()])),
});

let page: Page;

test.beforeAll(async ({ browser }) => {
  const context = await browser.newContext();
  page = await context.newPage();

  await page.route(
    /.+lb\.drpc\.org\/ogrpc\?network=ethereum.+/,
    async (route) => {
      if (route.request().method() === "POST") {
        await route.abort();
        return;
      }
    }
  );
});

test("Open Sushi Swap", async () => {
  // 等待请求应该在.goto()方法之前,
  // 因为所需的请求可能在页面完全加载之前完成。
  const requestPromise = page.waitForRequest(
    (request) =>
      request.url().includes("lb.drpc.org/ogrpc?network=ethereum") &&
      request.method() === "POST"
  );

  await page.goto("/swap");

  const request = await requestPromise;
  await expect(
    () => schema.parse(request.postDataJSON()),
    "Should have a request by the contract"
  ).not.toThrowError();
});

其中,

  • const schemaZod格式的模式声明;
  • beforeAll钩子中,所有匹配https://lb.drpc.org/ogrpc?network=ethereum&dkey=Ak765fp4zUm6uVwKu4annC8M80dnCZkR7pAEsm6XXi_w的 POST 请求都被阻止;
  • const requestPromise接收匹配https://lb.drpc.org/ogrpc?network=ethereum&dkey=Ak765fp4zUm6uVwKu4annC8M80dnCZkR7pAEsm6XXi_w的第一个请求的数据;
  • expect()断言中,参考模式与请求的数据进行解析。如果解析/验证过程没有失败,测试就通过 - [toThrowError()](https://jestjs.io/docs/expect#tothrowerror)

上面呈现的测试可能包含更多步骤和检查,因为契约检查可能只是端到端套件的一部分。

阅读更多关于契约测试的内容:

此外,理论上,相同的模拟方法可以应用于前端的所有 HTTP API 请求:

图片8: 模拟API

来源

来源

发布时间: 2023-12-25