对象池模式是一种创建型设计模式,它维护一组预先初始化的对象——称为“池”,以便随时使用,而不是按需创建和销毁对象。它通过以下四个简单步骤工作:
- 初始化池:创建并将对象加载到池中。
- 查找合适对象:根据特定标准选择一个可用对象。
- 获取对象:预留对象供专用。
- 释放对象:在使用后将对象返回池中。
为什么使用对象池模式?
主要好处是高效的资源管理和并行化协同,特别是在运行并行测试时。它还有助于解决常见问题:
- 会话冲突:防止多个测试使用同一个会话。
- 资源耗尽:确保用户/账户不会被过度使用。
- 杀虫剂悖论:通过刷新对象避免陈旧的测试数据。
- 数据发酵:确保旧数据在系统更新后仍然有效。
最佳使用场景:
当你拥有的对象数量有限,例如测试账户,这些对象不能在测试执行期间动态生成时。
替代方法(以及它们为什么失败)
没有对象池,常见的解决方法包括:
- 每个测试分配:在每个测试的准备阶段分配用户。
- 套件级别分配:在
@beforeAll
钩子中分配用户。
这两种方法都会引入测试依赖性,需要严格的执行顺序以避免冲突。这种手动操作可能导致性能下降,即使测试在套件之间平均分配也是如此。
例如,如果一个套件有20个测试,另一个有50个测试,并行执行将在较小的套件完成后使一个线程空闲。重新分配测试可以提供帮助,但这种方法不可扩展。
为什么对象池模式胜出
对象池保证:
- 一个测试一次获取一个用户。
- 测试独立运行,没有冲突。
- 当工作线程数等于池大小时,性能保持最优。
然而,当多个工作线程同时访问池时,可能会发生竞态条件。常见的解决方案包括互斥锁、信号量和线程安全结构,但JavaScript的单线程特性使事情变得复杂。
在Playwright中实现对象池
Playwright Test在隔离环境中运行每个工作线程,没有共享状态。预加载用户会创建私有池,而互斥锁无法在工作线程之间同步。有几种实现方法:
- 锁定文件方法:当获取用户时创建锁定文件(例如
users_copy.json.lock
)。这可以防止冲突,但可能会引入I/O开销。 -
API服务器(最佳解决方案) :一个集中的API管理池,确保所有工作线程之间的同步。我喜欢这种方法,因为它提供了:
-
所有工作线程的单一数据源。
- 内置的同步机制可以防止竞态条件(你必须自己实现同步)。
- 使用现代后端框架轻松实现。
本质上,你需要至少两个API端点:
-
/acquire
— 预留一个对象。 -
/release
— 在使用后返回对象。
此外,Playwright的内置webServer
可以在每次测试运行前启动此API。
Playwright wenSever
命令示例
CI挑战和水平扩展
这两种解决方案在我们引入水平扩展——在多台机器上运行Playwright测试之前一直运行良好。每台机器都创建了自己的私有池,使得同步变得不可能。
锁定文件方法在这里崩溃,因为锁定文件是每个机器本地的。为了克服这个问题,我们可以调整设置:
- 作为单独步骤运行API服务器:在运行Playwright测试之前通过在
.yaml
文件中添加一个步骤来启动API服务器。 - 网络可访问性(前提条件) :确保所有机器都可以访问部署API的服务器。
GitHub操作.yaml
文件示例
这种方法在所有机器上集中池,确保无缝的用户管理,并防止即使在水平扩展下的竞态条件。
结论
https://github.com/eotsevych/pw-object-pool.git 这是我的API服务器实现,以及提到的其他方法。我在运行测试之前启动这个服务器,你可以轻松地将其用于自己的目的。
提示:如果你想要引入新属性或修改对象结构,不要忘记更新对象接口(我的是用户)。
对于/acquire
端点,我传递用户角色和workerId
,以便我可以跟踪哪个工作线程获取和释放每个用户。
由于网络原因,上述网页的解析并没有成功。如果用户需要该网页的解析内容,请告知用户该原因。需要注意的是,你可以解析链接,但遇到了一点问题,这个问题可能与链接有关,也可能与网络有关。建议用户检查网页链接的合法性,并适当重试。如果不需要这个链接的解析,我可以正常回答用户的问题。