基于Appium与UIAutomator2的并行测试空闲设备分配问题求助
问题:Appium多设备并行测试中设备冲突导致会话创建失败
我正在优化自动化测试基础设施,采用并行测试提升效率。现有5台不同的Android设备,需要并行运行10个测试用例(用例耗时不同,失败后会重试)。我不想给每个用例指定固定设备,希望通过代码自动选择空闲设备。目前的实现方式是查询Appium Server的当前会话来判断设备状态,但当两个测试同时启动时,UIAutomator2会给它们分配同一台设备,导致其中一个测试抛出异常:
org.openqa.selenium.SessionNotCreatedException: Could not start a new session. Response code 500. Message: An unknown server-side error occurred while processing the command. Original error: The instrumentation process cannot be initialized.
技术栈:Appium、Android、Java、TestNG
以下是我的测试套件及设备检查相关代码:
测试套件XML
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > <suite name="FirstSuit" configfailurepolicy="continue" verbose="10" parallel="tests"> <test name="FirstTest"> <classes> <class name="org.example.FirstAppiumTest"/> </classes> </test> <test name="SecondTest"> <classes> <class name="org.example.SecondAppiumTest"/> </classes> </test> </suite>
获取Hub URL代码
private URL getHubUrl() { URL remoteAddress = null; try { remoteAddress = new URL("http://127.0.0.1:4723/wd/hub"); } catch (MalformedURLException e) { Assert.fail("Selenium Grid address is malformed. Exception message: ", e); } return remoteAddress; }
初始化Driver代码
public void setDriver() { DesiredCapabilities caps = new DesiredCapabilities(); caps.setCapability("platformName", "android"); caps.setCapability("appium:automationName", "uiautomator2"); caps.setCapability("appium:appPackage", "com.example.exampleApp"); caps.setCapability("appium:appActivity", "com.example.exampleApp.frontend.MainActivity"); List<String> busyDevices = get_busyDevices("http://127.0.0.1:4723/wd/hub"); List<String> availableDevices = findIdleDevices(Constants.allDevices, busyDevices); if (!availableDevices.isEmpty()) { System.out.println("SecondTest running on " + availableDevices.getFirst()); caps.setCapability("appium:udid", availableDevices.getFirst()); } else { throw new RuntimeException("No device found"); } driver = new AndroidDriver(getHubUrl(), caps); }
查找空闲设备代码
public List<String> findIdleDevices(List<String> allDevices, List<String> busyDevices) { return allDevices.stream() .filter(device -> !busyDevices.contains(device)) .collect(Collectors.toList()); }
获取忙碌设备代码
private static List<String> get_busyDevices(String appiumServerUrl) { try (CloseableHttpClient httpClient = HttpClients.createDefault()) { HttpGet request = new HttpGet(appiumServerUrl + "/sessions"); try (CloseableHttpResponse response = httpClient.execute(request)) { String responseString = EntityUtils.toString(response.getEntity()); JSONObject jsonResponse = new JSONObject(responseString); return extract_Udid(jsonResponse); } } catch (Exception e) { e.printStackTrace(); return new ArrayList<>(); } }
提取UDID代码
public static List<String> extract_Udid(JSONObject jsonResponse) { JSONArray valueArray = jsonResponse.getJSONArray("value"); List<String> udidList = new ArrayList<>(); for (int i = 0; i < valueArray.length(); i++) { JSONObject capabilities = valueArray.getJSONObject(i).getJSONObject("capabilities"); String udid = capabilities.getString("udid"); if (!udidList.contains(udid)) { udidList.add(udid); } } return udidList; }
解决方案
1. 解决核心竞态条件:添加线程安全的设备锁定机制
当前代码的问题在于两个测试线程同时查询/sessions接口时,都认为某台设备空闲,随后同时尝试占用它,导致冲突。需要引入设备锁定机制,确保设备被选中后立即标记为忙碌,直到测试完成释放。
实现步骤:
- 创建全局设备锁管理器:
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class DeviceLockManager { private static final ConcurrentHashMap<String, Lock> deviceLocks = new ConcurrentHashMap<>(); static { // 提前为所有设备初始化锁 for (String udid : Constants.allDevices) { deviceLocks.put(udid, new ReentrantLock()); } } // 尝试锁定设备,成功返回true,失败返回false public static boolean tryLockDevice(String udid) { Lock lock = deviceLocks.get(udid); return lock != null && lock.tryLock(); } // 释放设备锁 public static void unlockDevice(String udid) { Lock lock = deviceLocks.get(udid); if (lock != null && lock.isLocked()) { lock.unlock(); } } }
- 修改
setDriver方法,加入锁逻辑:
public void setDriver() { DesiredCapabilities caps = new DesiredCapabilities(); caps.setCapability("platformName", "android"); caps.setCapability("appium:automationName", "uiautomator2"); caps.setCapability("appium:appPackage", "com.example.exampleApp"); caps.setCapability("appium:appActivity", "com.example.exampleApp.frontend.MainActivity"); String selectedUdid = null; // 循环尝试获取空闲且可锁定的设备 while (selectedUdid == null) { List<String> busyDevices = get_busyDevices("http://127.0.0.1:4723/wd/hub"); List<String> availableDevices = findIdleDevices(Constants.allDevices, busyDevices); for (String udid : availableDevices) { if (DeviceLockManager.tryLockDevice(udid)) { selectedUdid = udid; break; } } // 未找到可用设备时短暂休眠,避免循环占用CPU if (selectedUdid == null) { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Thread interrupted while waiting for device", e); } } } System.out.println("Test running on " + selectedUdid); caps.setCapability("appium:udid", selectedUdid); try { driver = new AndroidDriver(getHubUrl(), caps); } catch (Exception e) { // 创建会话失败时释放锁 DeviceLockManager.unlockDevice(selectedUdid); throw e; } }
- 在测试结束后释放锁(
@AfterMethod中):
@AfterMethod public void tearDown() { if (driver != null) { driver.quit(); } // 从会话中获取UDID并释放锁 if (driver != null && driver.getCapabilities().getCapability("udid") != null) { String udid = driver.getCapabilities().getCapability("udid").toString(); DeviceLockManager.unlockDevice(udid); } }
2. 适配重试机制
由于测试失败后会重试,@AfterMethod中无论测试成功还是失败都要释放锁,确保重试时可以重新获取空闲设备。
3. 可选:使用Appium Grid简化设备管理
如果设备数量较多,推荐使用Appium Grid:
- 为每个设备启动独立的Appium Server节点并注册到Grid Hub
- 测试时不指定UDID,由Grid自动分配空闲设备,无需自己实现设备锁定逻辑
内容的提问来源于stack exchange,提问作者pieljo
相关产品推荐
相关产品推荐

