Docker环境下结合Selenium Grid实现JUnit5版Cucumber测试分布式分发咨询
实现Cucumber JUnit5与Selenium Grid Docker容器的绑定调度
基础配置对齐
你已经掌握的cucumber.execution.parallel.enabled=true是并行调度的基础,只需要根据目标的并行粒度,补充对应的配置到junit-platform.properties文件中,即可让Cucumber JUnit5的调度单元匹配到你需要的拆分粒度,不需要额外引入TestNG相关的调度逻辑。
核心逻辑:端口分配与WebDriver初始化的隔离机制
TestNG场景下的端口传参逻辑,在Cucumber JUnit5生态下可以通过线程隔离上下文 + 线程安全端口池 + JUnit5扩展钩子复刻,完全避免多并行任务下的端口串用、WebDriver会话不匹配的问题:
- 搭建线程隔离的端口存储上下文,因为JUnit5的并行调度单元(不管是特性还是场景)都会运行在独立线程上,天然满足隔离要求:
public class GridPortContext { // 线程隔离存储当前执行单元绑定的Grid节点端口 private static final ThreadLocal<Integer> CURRENT_BOUND_PORT = new ThreadLocal<>(); public static void setPort(int port) { CURRENT_BOUND_PORT.set(port); } public static int getPort() { return CURRENT_BOUND_PORT.get(); } public static void clear() { CURRENT_BOUND_PORT.remove(); } } - 搭建线程安全的端口池,提前把docker-compose动态映射的所有Selenium节点端口存入队列,保证每个并行执行单元只会拿到一个独占的、未被其他任务占用的端口:
// 端口池配置,从docker-compose动态读取映射端口后初始化 public class GridPortPool { private static final Queue<Integer> AVAILABLE_PORTS = new ConcurrentLinkedQueue<>(); public static void initPorts(List<Integer> mappedPorts) { AVAILABLE_PORTS.clear(); AVAILABLE_PORTS.addAll(mappedPorts); } public static Integer allocatePort() { return AVAILABLE_PORTS.poll(); } public static void releasePort(Integer port) { AVAILABLE_PORTS.offer(port); } } - 实现JUnit5扩展钩子,在每个并行调度单元的生命周期内完成端口分配与释放,替换TestNG的参数注入逻辑:
public class GridPortLifecycleExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { @Override public void beforeTestExecution(ExtensionContext context) { // 执行前从端口池分配独占端口,存入线程上下文 Integer allocatedPort = GridPortPool.allocatePort(); GridPortContext.setPort(allocatedPort); } @Override public void afterTestExecution(ExtensionContext context) { // 执行完成后释放端口回到池子,清理线程上下文 Integer usedPort = GridPortContext.getPort(); GridPortPool.releasePort(usedPort); GridPortContext.clear(); } } - 按照分配的端口初始化RemoteWebDriver,完全复刻原TestNG案例的端口驱动逻辑:
public class SeleniumDriverFactory { public static RemoteWebDriver createDriver() { int boundPort = GridPortContext.getPort(); // 拼接当前绑定端口的Grid节点访问地址 URL gridEndpoint = new URL("http://localhost:" + boundPort + "/wd/hub"); ChromeOptions chromeOptions = new ChromeOptions(); // 补充浏览器的无头、沙箱关闭等适合容器运行的参数 chromeOptions.addArguments("--headless=new"); chromeOptions.addArguments("--no-sandbox"); return new RemoteWebDriver(gridEndpoint, chromeOptions); } }
按需求匹配调度粒度,实现两个核心目标
目标1:每个特性文件匹配独立容器执行
需要调整junit-platform.properties的配置,让调度单元绑定为整个特性文件,而非单个场景:
# 开启并行执行 cucumber.execution.parallel.enabled=true # 固定并行数,和你拉起的Selenium容器数完全对齐 cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=3 # 让整个特性文件内的所有场景共享同一个执行线程,绑定同一个端口 cucumber.execution.execution.mode=same_thread
同时调整生命周期钩子的触发时机,把端口分配的逻辑从单场景级移到特性级:通过Cucumber的BeforeAll钩子在特性启动时分配端口,在AfterAll钩子中释放端口,保证整个特性的所有场景都使用同一个Grid容器。
目标2:每个测试场景单独分配独立容器执行
调整junit-platform.properties的配置,让调度单元拆分为单个场景:
# 开启并行执行 cucumber.execution.parallel.enabled=true # 动态调整并行度,匹配端口池的可用容量 cucumber.execution.parallel.config.strategy=dynamic cucumber.execution.parallel.config.dynamic.factor=1 # 取消特性文件的线程绑定,让每个场景独立运行在单独线程 cucumber.execution.execution.mode=same_thread_per_scenario
此时保持之前的单场景级端口分配钩子即可,每个场景执行前会自动从端口池拿一个独占端口,对应独立的Selenium容器,执行完成后自动释放端口供其他场景使用。
Azure Pipeline环境适配注意事项
- 用docker-compose拉起Selenium节点时,不要硬编码固定端口映射,改用动态端口范围配置,在管道任务启动时通过docker inspect读取所有节点的宿主机映射端口,动态注入到
GridPortPool中,避免硬编码端口和容器实际映射不匹配的问题。 - 给每个Selenium节点配置单会话上限(设置环境变量
SE_NODE_MAX_SESSIONS=1),保证每个容器同一时间只承接一个测试任务,和端口独占的逻辑完全匹配。 - 不要跨线程传递WebDriver实例,保持每个线程的WebDriver和绑定的端口一一对应,避免出现会话串接、操作不对应的问题。
内容的提问来源于stack exchange,提问作者user1207289
相关产品推荐
相关产品推荐

