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

Appium JUnit多设备并行测试异常:单设备运行/设备串位求助

Appium多设备并行测试问题排查与修复

我正在尝试用JUnit实现Appium多设备并行测试,计划让两台设备同时执行测试,但目前每次只有一台设备在运行测试;更糟的是,有时日志显示测试运行在设备X上,实际操作却发生在设备Y上。我现在通过JSON文件存储deviceID,用队列机制来管理设备的占用状态,直到测试完成。我的代码如下:

package core.managers;

import api.android.Android;
import core.ADB;
import core.MyLogger;
import core.Timer;
import core.constants.Arg;
import core.constants.Resources;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.service.local.AppiumServiceBuilder;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.service.DriverService;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

public class DriverManager {
    private static String nodeJS = "C:/Program Files/nodejs/node.exe";
    private static String appiumJS = "C:/Appium/resources/app/node_modules/appium/build/lib/main.js";
    private static DriverService service;
    private static String deviceID;
    private static HashMap<String, URL> hosts;
    private static String unlockPackage = "io.appium.unlock";

    private static DesiredCapabilities getCaps(String deviceID){
        MyLogger.log.info("Creating driver caps for device: "+deviceID);
        DesiredCapabilities caps = new DesiredCapabilities();
        caps.setCapability("deviceName", deviceID);
        caps.setCapability("platformName", "Android");
        caps.setCapability("app", "C:/Users/maoz/Desktop/spotter89.apk");
        caps.setCapability("appPackage", "com.mobileresearchlabs.spotter.ex.no");
        caps.setCapability("appActivity", ("com.mobileresearchlabs.spotter.SpotterActivity"));
        caps.setCapability("appWaitActivity", "com.mobileresearchlabs.spotter.SplashActivity");
        return caps;
    }

    private static URL host(String deviceID) throws MalformedURLException {
        if(hosts == null){
            hosts = new HashMap<String, URL>();
            hosts.put("ffc64100", new URL("http://127.0.0.1:4723/wd/hub"));
            hosts.put("4d00b9e74ed431df", new URL("http://127.0.0.1:4724/wd/hub"));
        }return hosts.get(deviceID);
    }

    private static ArrayList<String> getAvailableDevices(){
        MyLogger.log.info("Checking for available devices");
        ArrayList<String> avaiableDevices = new ArrayList<String>();
        ArrayList connectedDevices = ADB.getConnectedDevices();
        for(Object connectedDevice: connectedDevices){
            String device = connectedDevice.toString();
            ArrayList apps = new ADB(device).getInstalledPackages();
            if(!apps.contains(unlockPackage)){
                if(useDevice(deviceID))
                    avaiableDevices.add(device);
                else MyLogger.log.info("Device: "+deviceID+" is being used by another JVM");
            } else MyLogger.log.info("Device: "+device+" has "+unlockPackage+" installed, assuming it is under testing");
        }
        if(avaiableDevices.size() == 0)
            throw new RuntimeException("Not a single device is available for testing at this time");
        return avaiableDevices;
    }

    private static DriverService createService() throws MalformedURLException {
        service = new AppiumServiceBuilder()
                .usingDriverExecutable(new File(nodeJS))
                .withAppiumJS(new File(appiumJS))
                .withIPAddress(host(deviceID).toString().split(":")[1].replace("//", ""))
                .usingPort(Integer.parseInt(host(deviceID).toString().split(":")[2].replace("/wd/hub","")))
                .withArgument(Arg.TIMEOUT, "120")
                .withArgument(Arg.LOG_LEVEL, "warn")
                .build();
        return service;
    }

    public static void createDriver() throws MalformedURLException {
        ArrayList<String> devices = getAvailableDevices();
        for(String device : devices){
            try{
                deviceID = device;
                if(useDevice(deviceID)){
                    queueUp();
                    gracePeriod();
                    MyLogger.log.info("Trying to create new Driver for device: "+device);
                    createService().start();
                    Android.driver = new AndroidDriver(host(device), getCaps(device));
                    Android.adb = new ADB(device);
                    leaveQueue();
                    break;
                }
            }catch (Exception e){
                e.printStackTrace(); //Ignore and try next device
            }
        }
    }

    public static void killDriver(){
        if(Android.driver != null){
            MyLogger.log.info("Killing Android Driver");
            Android.driver.quit();
            Android.adb.uninstallApp(unlockPackage);
            service.stop();
        }else MyLogger.log.info("Android Driver is not initialized, nothing to kill");
    }

    private static void queueUp() {
        try {
            MyLogger.log.info("Queueing Up: "+deviceID);
            JSONObject json = new JSONObject();
            json.put("queued_at", Timer.getTimeStamp());
            JSONObject jsonQueue = Resources.getQueue();
            jsonQueue.put(deviceID, json);
            MyLogger.log.info("JSON Queue: "+jsonQueue);
            ServerManager.write(new File(Resources.QUEUE), jsonQueue.toString());
        } catch (IOException | ParseException e) {
            throw new RuntimeException(e);
        }
    }

    private static boolean useDevice(String deviceID) {
        try {
            JSONObject json = Resources.getQueue();
            if(json.containsKey(deviceID)){
                JSONObject deviceJson = (JSONObject) json.get(deviceID);
                // 代码此处截断,缺失后续判断逻辑

问题根源分析

从代码和现象来看,核心问题出在线程安全和资源竞争上:

  1. 静态变量共享冲突:deviceID、service这些静态变量会被所有测试线程共享,当多个线程并行执行时,线程A的deviceID可能被线程B覆盖,导致日志显示设备X但实际操作设备Y。
  2. 队列操作无同步:useDevice、queueUp等方法读写JSON队列时没有加锁,多个线程可能同时读取到同一设备为"可用",导致设备被重复占用。
  3. 服务与设备绑定错误:createService依赖静态的deviceID,线程切换时会导致服务绑定到错误的端口/设备。
  4. 代码不完整:useDevice方法缺失后续判断逻辑,无法正确识别设备是否被占用或超时释放。

修复方案

1. 用ThreadLocal隔离线程资源

把所有线程共享的静态变量改为ThreadLocal,确保每个测试线程拥有独立的实例:

private static ThreadLocal<String> deviceID = new ThreadLocal<>();
private static ThreadLocal<DriverService> service = new ThreadLocal<>();
// 同时修改Android类中的driver为ThreadLocal
public static ThreadLocal<AndroidDriver> driver = new ThreadLocal<>();

2. 同步队列读写操作

给JSON队列的读写加锁,避免竞态条件:

private static final Object QUEUE_LOCK = new Object();

private static boolean useDevice(String deviceID) {
    synchronized (QUEUE_LOCK) {
        try {
            JSONObject json = Resources.getQueue();
            if(json.containsKey(deviceID)){
                JSONObject deviceJson = (JSONObject) json.get(deviceID);
                long queuedAt = (long) deviceJson.get("queued_at");
                long currentTime = System.currentTimeMillis();
                // 超过5分钟未释放则视为可用(可根据测试时长调整)
                if(currentTime - queuedAt > 300000){
                    json.remove(deviceID);
                    ServerManager.write(new File(Resources.QUEUE), json.toString());
                    return true;
                }
                return false;
            } else {
                return true;
            }
        } catch (IOException | ParseException e) {
            throw new RuntimeException(e);
        }
    }
}

private static void queueUp() {
    synchronized (QUEUE_LOCK) {
        try {
            String currentDevice = deviceID.get();
            MyLogger.log.info("Queueing Up: "+currentDevice);
            JSONObject json = new JSONObject();
            json.put("queued_at", Timer.getTimeStamp());
            JSONObject jsonQueue = Resources.getQueue();
            jsonQueue.put(currentDevice, json);
            ServerManager.write(new File(Resources.QUEUE), jsonQueue.toString());
        } catch (IOException | ParseException e) {
            throw new RuntimeException(e);
        }
    }
}

private static void leaveQueue() {
    synchronized (QUEUE_LOCK) {
        try {
            JSONObject jsonQueue = Resources.getQueue();
            jsonQueue.remove(deviceID.get());
            ServerManager.write(new File(Resources.QUEUE), jsonQueue.toString());
        } catch (IOException | ParseException e) {
            throw new RuntimeException(e);
        }
    }
}

3. 让Appium服务与设备一一绑定

修改createService方法,让它接收deviceID参数,不再依赖静态变量:

private static DriverService createService(String deviceID) throws MalformedURLException {
    URL deviceHost = host(deviceID);
    return new AppiumServiceBuilder()
            .usingDriverExecutable(new File(nodeJS))
            .withAppiumJS(new File(appiumJS))
            .withIPAddress(deviceHost.getHost())
            .usingPort(deviceHost.getPort())
            .withArgument(Arg.TIMEOUT, "120")
            .withArgument(Arg.LOG_LEVEL, "warn")
            .build();
}

同时更新createDriver方法,确保线程资源独立:

public static void createDriver() throws MalformedURLException {
    ArrayList<String> devices = getAvailableDevices();
    for(String device : devices){
        try{
            deviceID.set(device);
            if(useDevice(device)){
                queueUp();
                gracePeriod();
                MyLogger.log.info("Trying to create new Driver for device: "+device);
                DriverService currentService = createService(device);
                service.set(currentService);
                currentService.start();
                Android.driver.set(new AndroidDriver(host(device), getCaps(device)));
                Android.adb = new ADB(device); // 若adb也需要线程隔离,同样改为ThreadLocal
                leaveQueue();
                break;
            }
        }catch (Exception e){
            e.printStackTrace();
            // 清理当前线程的资源
            deviceID.remove();
            if(service.get() != null){
                service.get().stop();
                service.remove();
            }
        }
    }
}

4. 完善killDriver的资源释放

修改killDriver方法,确保ThreadLocal资源被正确清理:

public static void killDriver(){
    AndroidDriver driver = Android.driver.get();
    DriverService currentService = service.get();
    String currentDevice = deviceID.get();

    if(driver != null){
        MyLogger.log.info("Killing Android Driver for device: "+currentDevice);
        driver.quit();
        new ADB(currentDevice).uninstallApp(unlockPackage);
        Android.driver.remove();
    }

    if(currentService != null){
        currentService.stop();
        service.remove();
    }

    if(currentDevice != null){
        leaveQueue();
        deviceID.remove();
    }
}

5. 配置JUnit并行执行

确保JUnit支持并行测试,比如用JUnit 5的注解:

import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

@Execution(ExecutionMode.CONCURRENT)
public class YourTestClass {
    // 测试用例
}

或者在junit-platform.properties中添加全局配置:

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent

内容的提问来源于stack exchange,提问作者Maoz Smadja

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 10:06:26