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); // 代码此处截断,缺失后续判断逻辑
问题根源分析
从代码和现象来看,核心问题出在线程安全和资源竞争上:
- 静态变量共享冲突:
deviceID、service这些静态变量会被所有测试线程共享,当多个线程并行执行时,线程A的deviceID可能被线程B覆盖,导致日志显示设备X但实际操作设备Y。 - 队列操作无同步:
useDevice、queueUp等方法读写JSON队列时没有加锁,多个线程可能同时读取到同一设备为"可用",导致设备被重复占用。 - 服务与设备绑定错误:
createService依赖静态的deviceID,线程切换时会导致服务绑定到错误的端口/设备。 - 代码不完整:
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
相关产品推荐
相关产品推荐

