停止资源泄漏:如何在 Playwright 测试中使用 AbortSignal
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 毫秒后自动中止的信号。 |
注意: 从该 包 中导入
test和expect,而不是从@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(signal、abortController、useAbortController、useSignalWithTimeout)。 - 结果: 没有孤立的 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);
});
工作原理
- 每个测试之前 – 通过 Playwright 的 fixture 系统创建一个全新的
AbortController。 - 控制器及其
signal以abortController和signalfixture 的形式暴露。 - 当测试超时 时,控制器会自动中止。
- 任何监听该信号的操作都会收到
AbortError并停止。 - 每个测试之后,控制器会被清理。
这意味着每个测试都有自己独立的取消作用域。一个测试超时 不会 影响其他测试。
最佳实践
- 不要自行创建
AbortController,因为 fixture 已经提供了一个。fixture 提供的控制器已与测试生命周期绑定,手动创建的控制器不会在超时自动中止。 - 始终将 signal 传递给异步操作(例如
fetch、数据库查询)。未受保护的调用如果没有 signal,就会出现僵尸请求问题。 - 不要悄悄吞掉
AbortError。当信号触发时,操作会以AbortError被拒绝。让 Playwright 处理超时报告,而不是捕获并隐藏错误。
安装
npm install @playwright-labs/fixture-abort
完整源码和文档:
该包是 @playwright-labs monorepo 的一部分。请从 @playwright-labs/fixture-abort 而不是 @playwright/test 导入 test 和 expect,abort fixture 将在每个测试中自动可用。
试一试吧——你的预发布服务器会感谢你的!