停止资源泄漏:如何在 Playwright 测试中使用 AbortSignal

发布: (2026年2月16日 GMT+8 19:19)
9 分钟阅读
原文: Dev.to

Source: Dev.to

(请提供需要翻译的正文内容,我将按照要求保留源链接、格式和代码块,仅翻译文本部分。)

问题

test('should fetch user profile', async () => {
  const response = await fetch('https://api.example.com/users/123');
  const user = await response.json();
  expect(user.name).toBe('Alice');
});
  • 如果测试超时(API 速度慢、服务器负载高等),Playwright 会停止测试 fetch 调用仍会继续运行,直到完成或达到它自己的超时(通常 30 秒以上)。

为什么在大规模时这很重要

  • 连接池耗尽 – 工作进程用完了套接字。
  • 服务器过载 – 你的预发布环境在处理没有人等待的请求。
  • 误导性的日志 – 孤立的请求会在服务器日志中显示为错误。
  • 连锁故障 – 一个耗尽的服务可能导致整个测试套件崩溃。

为什么 afterEach 没有帮助

afterEach 运行时,你已经不再拥有飞行中请求的引用;该 promise 被困在已超时的测试函数内部。你无法取消无法触及的东西。

解决方案: 在每个异步操作提前传入取消令牌(AbortSignal),然后在测试结束时触发它。

AbortController 与 AbortSignal

AbortController(在浏览器和 Node.js 中都是标准)会创建一个信号,可用于取消异步操作:

const controller = new AbortController();
const signal = controller.signal;

// 将信号传递给 fetch
await fetch('/api/data', { signal });

// 稍后取消
controller.abort(); // fetch 会因 AbortError 被拒绝

@playwright-labs/fixture-abort

安装

npm install @playwright-labs/fixture-abort

包提供的内容

夹具描述
abortController为每个测试提供一个全新的 AbortController 实例。
signal与之关联的 AbortSignal
useAbortController(options?)返回该控制器;你可以提供 onAbort 回调。
useSignalWithTimeout(ms)返回一个在 ms 毫秒后自动中止的信号。

注意: 从该 中导入 testexpect,而不是从 @playwright/test 导入,以自动获取这些夹具。

import { test, expect } from '@playwright-labs/fixture-abort';

使用示例

1️⃣ 自动取消的简单 fetch

import { test, expect } from '@playwright-labs/fixture-abort';

test('should fetch user profile', async ({ signal }) => {
  const response = await fetch('https://api.example.com/users/123', { signal });
  const user = await response.json();
  expect(user.name).toBe('Alice');
});

如果测试超时,signal 会触发,请求会立即被取消。

2️⃣ 轮询直至满足条件

import { test, expect } from '@playwright-labs/fixture-abort';

test('should wait for order processing', async ({ signal }) => {
  const orderId = await createOrder();

  while (!signal.aborted) {
    const response = await fetch(`/api/orders/${orderId}`, { signal });
    const order = await response.json();

    if (order.status === 'completed') {
      expect(order.total).toBeGreaterThan(0);
      return;
    }

    // 等待 2 秒后再进行下一次轮询
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
});

while (!signal.aborted) 这层守卫保证当测试被中止时循环能够干净地退出。

3️⃣ 并行请求 – 一个 signal,多个调用

import { test, expect } from '@playwright-labs/fixture-abort';

test('should fetch dashboard data', async ({ signal }) => {
  const [users, orders, metrics] = await Promise.all([
    fetch('/api/users',   { signal }),
    fetch('/api/orders',  { signal }),
    fetch('/api/metrics', { signal })
  ]);

  expect(users.ok).toBe(true);
  expect(orders.ok).toBe(true);
  expect(metrics.ok).toBe(true);
});

如果测试中止,所有三个请求会一起被取消。

4️⃣ 基于测试逻辑的手动 abort

import { test, expect } from '@playwright-labs/fixture-abort';

test('should stop on first error', async ({ signal, abortController }) => {
  const items = await getItemsToProcess();

  for (const item of items) {
    if (signal.aborted) break;

    const response = await fetch(`/api/process/${item.id}`, {
      method: 'POST',
      signal
    });

    if (!response.ok) {
      abortController.abort(); // 取消剩余工作
      break;
    }
  }
});

你可以在测试体内部中止整个测试。

5️⃣ useAbortController – 在 abort 时注册回调

import { test, expect } from '@playwright-labs/fixture-abort';

test('should handle abort with cleanup', async ({ useAbortController, signal }) => {
  const controller = useAbortController({
    onAbort: () => console.log('Operation cancelled, cleaning up'),
    abortTest: true               // 可选 – 同时中止测试本身
  });

  const response = await fetch('/api/long-operation', { signal });
  const data = await response.json();
  expect(data).toBeDefined();
});

当 signal 被触发时,onAbort 钩子会自动执行。

6️⃣ useSignalWithTimeout – 固定时长后自动 abort

import { test, expect } from '@playwright-labs/fixture-abort';

test('should auto‑abort after 10 seconds', async ({ useSignalWithTimeout }) => {
  const signal = useSignalWithTimeout(10_000); // 10 秒

  const response = await fetch('/api/slow-endpoint', { signal });
  const data = await response.json();
  expect(data).toBeDefined();
});

信号会在指定的超时时间后自动中止,即使测试本身没有超时,也能防止请求失控。

TL;DR

  • 问题: Playwright 测试中的长时间运行的异步工作在测试超时后仍会继续运行,导致资源泄漏。
  • 解决方案: 为每个异步操作传入 AbortSignal,并在测试结束时中止它。
  • 工具: @playwright-labs/fixture-abort 为你提供现成的 fixture(signalabortControlleruseAbortControlleruseSignalWithTimeout)。
  • 结果: 没有孤立的 HTTP 请求,没有连接池耗尽,CI 运行更干净。

祝测试愉快! 🚀

# Abort Fixtures for Playwright

```ts
import { test, expect } from '@playwright-labs/fixture-abort';

test('should complete within 5 seconds', async ({ useSignalWithTimeout }) => {
  const timeoutSignal = useSignalWithTimeout(5000);

  const response = await fetch('/api/slow-endpoint', {
    signal: timeoutSignal,
  });
  expect(response.ok).toBe(true);
});

许多现代库都接受 AbortSignal。你可以把该信号传给任何支持它的操作:

import { test, expect } from '@playwright-labs/fixture-abort';

test('should query database', async ({ signal }) => {
  // 许多数据库客户端都接受 abort signal
  const result = await db.query('SELECT * FROM users', {
    signal,
  });
  expect(result.rows.length).toBeGreaterThan(0);
});

这适用于:

  • Axios(通过 signal 选项)
  • Node.js 的 fetch 实现
  • 许多数据库驱动
  • gRPC 客户端
  • …以及更多。

该包还提供了自定义 expect 匹配器,用于测试 abort 状态:

import { test, expect } from '@playwright-labs/fixture-abort';

test('should verify abort state', async ({ signal, abortController }) => {
  expect(signal).toBeActive();

  abortController.abort('test reason');

  expect(signal).toBeAborted();
  expect(signal).toBeAbortedWithReason('test reason');
  expect(abortController).toHaveAbortedSignal();
});

test('should verify timeout signal aborts', async ({ useSignalWithTimeout }) => {
  const timeoutSignal = useSignalWithTimeout(100);
  await expect(timeoutSignal).toAbortWithin(150);
});

工作原理

  1. 每个测试之前 – 通过 Playwright 的 fixture 系统创建一个全新的 AbortController
  2. 控制器及其 signalabortControllersignal fixture 的形式暴露。
  3. 当测试超时 时,控制器会自动中止。
  4. 任何监听该信号的操作都会收到 AbortError 并停止。
  5. 每个测试之后,控制器会被清理。

这意味着每个测试都有自己独立的取消作用域。一个测试超时 不会 影响其他测试。

最佳实践

  • 不要自行创建 AbortController,因为 fixture 已经提供了一个。fixture 提供的控制器已与测试生命周期绑定,手动创建的控制器不会在超时自动中止。
  • 始终将 signal 传递给异步操作(例如 fetch、数据库查询)。未受保护的调用如果没有 signal,就会出现僵尸请求问题。
  • 不要悄悄吞掉 AbortError。当信号触发时,操作会以 AbortError 被拒绝。让 Playwright 处理超时报告,而不是捕获并隐藏错误。

安装

npm install @playwright-labs/fixture-abort

完整源码和文档:

该包是 @playwright-labs monorepo 的一部分。请从 @playwright-labs/fixture-abort 而不是 @playwright/test 导入 testexpect,abort fixture 将在每个测试中自动可用。

试一试吧——你的预发布服务器会感谢你的!

0 浏览
Back to Blog

相关文章

阅读更多 »