You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 12:45:53