一、应用场景:
你是否有这样的困惑:通过appium+testng已经写好一个个移动端自动化用例了,单个用例运行也没有什么问题,但是真实的企业应用场景是筛选一批合适的用例同时运行,无人值守,那下面这个案例将是一个本人已实践过的方案,希望能带给你相关的思考。
二、本文案例环境配置:
搭建好openSTF服务,并连接至少2台移动设备(或模拟器)。
http://www.360doc.com/showweb/0/0/983537700.aspx
编写appium+testng安卓端自动化用例。
安装nodejs并通过nodejs安装appium。
http://www.360doc.com/content/21/0528/18/8189294_979429344.shtml
基本思路图:
注:
本文仅作为一个思路引导,真正的实践其实是根据公司现有的自动化成果来做调整的,读者可将里面对你有用的东西引入进你自己的自动化中。
接下来将根据上述图示来写代码,本文的重点是与openStf相关的内容,关于appium自动化用例如何编写,appium如何批量运行,怎样介入mq等不在本文范围。
三、appium并发执行的2种思路
3.1、 hub--node的方式(即selenium grid + appium node)
如下图,它的grid其实就用的是selenium的grid,他们是通用的,只是web端你需要将浏览器node关联至grid,而移动端与grid关联的就是一个个appium server,每个appium server再去连接一台移动设备。
此种方式需要在用例运行前即把每台移动设备与appium server连接好,然后用例运行时grid即去将用例分发至每个appium node上去执行。
我们当前没有采用该种方式,考虑到移动设备需要充分利用,使用该种方式有局限性。---从当前了解到的知识来看,后续或许能找到相关解决方案。
3.2、动态启appium服务(随时用随时启,只要有空闲移动设备)
这是由appium本身的工作机制决定,因为一个appium服务,只能与一台移动设备通信,而要实现并发,那就需要多启几个服务,且每个服务都要连接一台手机。
而在本案例中即通过在用例执行时去动态启动appium服务,并连接到空闲移动设备。
四、代码实现
1)启动appium server
public class AppiumServer { private static AppiumServiceBuilder appiumServicebuilder; private static AppiumDriverLocalService appiumService; public static URL getAppiumUrl (){ logger.info("获取Ip:"+appiumService.getUrl().toString()); return appiumService.getUrl(); } public static void startServer(){ //启动appium服务 appiumServicebuilder = new AppiumServiceBuilder(); appiumServicebuilder.withIPAddress("127.0.0.1");//此处ip无法动态,手机必须连接在服务启动的电脑 int port = Integer.parseInt("47"+ RandomUtil.nextInt(89)); logger.info("Random appium server port is:"+port); if (!checkAppiumServerIsRunning(port)){ appiumServicebuilder.usingPort(port);//需要改成动态的 } appiumServicebuilder.withArgument(GeneralServerFlag.SESSION_OVERRIDE); appiumServicebuilder.withArgument(GeneralServerFlag.LOG_LEVEL,"error");//暂时不知道是哪里的日志级别 //appiumService = AppiumDriverLocalService.buildDefaultService(); appiumService = AppiumDriverLocalService.buildService(appiumServicebuilder); appiumService.start(); } }
2)启动appium driver
public class MyAndroidKeyMobileDriver{ /* * 启动driver */ public static WebDriver getDriver(ITestContext context) { WebDriver driver = null; DesiredCapabilities desiredCapabilities = null; try { desiredCapabilities = BaseDriven.setAndroidCapabilities(context); //新增的代码 driver = new AndroidDriver<>(AppiumServer.getAppiumUrl(), desiredCapabilities); DriverCache.set(driver); driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS); logger.info("appium android 成功启动 APP Package [ " + desiredCapabilities.getCapability(AndroidMobileCapabilityType.APP_PACKAGE) + " ]。"); }catch ( WebDriverException e ) { if (e.getMessage().contains("Connection refused: ")) { logger.error("Appium Desktop 服务器未启动,请检查,谢谢。"); } if (e.getMessage().contains("not find app package")) { assert desiredCapabilities != null; logger.error("Android app【 " + desiredCapabilities.getCapability (AndroidMobileCapabilityType.APP_PACKAGE) + " ] 不存在。"); } logger.error("App启动异常:", e); } return driver; } }
3)通过stf api操作设备
注:stf提供了关于操作所挂载的设备相关接口,只要把stf服务启动起来即可访问。
/* * 获取所有设备 */ public static List<DeviceEntity> getAllDevices(){ Map stfHeaderMap = new HashMap(); Map paramMap = new HashMap(); stfHeaderMap.put("Authorization","Bearer "+ACCESS_TOKEN); String response = httpClientAdaptor.doGet(STF_SERVICE_URL + "devices",stfHeaderMap,paramMap);//查询所有的设备 Map maps = JsonUtil.toObject(response, Map.class); // json转换为Map String devices = String.valueOf(maps.get("devices")); // 获取设备列表,是一个json数组 System.out.println("所有的设备信息:"+devices); //将json数组转化为 List<DeviceEntity> deviceList = new ArrayList<>(); JSONArray array = JsonUtil.toJSONArray(devices); for (int i = 0;i<array.size();i++){ DeviceEntity deviceEntity = new DeviceEntity(); JSONObject obj = (JSONObject) array.get(i); deviceEntity.setSerial(obj.getString("serial")); deviceEntity.setPlatform(obj.getString("platform")); deviceEntity.setVersion(obj.getString("version")); deviceEntity.setRemoteConnectUrl(obj.getString("remoteConnectUrl")); deviceEntity.setUsing(Boolean.parseBoolean(obj.getString("using"))); deviceEntity.setReady(Boolean.parseBoolean(obj.getString("ready"))); deviceEntity.setPresent(Boolean.parseBoolean(obj.getString("present"))); String providerJson = obj.getString("provider"); Map providerMaps = JsonUtil.toObject(providerJson, Map.class); String providerName = (String) providerMaps.get("name"); deviceEntity.setProvider(providerName); deviceList.add(deviceEntity); } System.out.println("设备个数:"+deviceList.size()); return deviceList; } /* * 查找空闲设备并锁定 */ public static String connectFreeDevice(String slaveMachine){ String currentConnectedDevice = null; List<DeviceEntity> deviceList = getAllDevices(); for (Iterator itr = deviceList.iterator(); itr.hasNext();) { DeviceEntity device = (DeviceEntity) itr.next(); //获取对应编译机上挂载的手机设备 if (device.getProvider().contains(slaveMachine)){ if (!device.isPresent() || !device.isReady() || device.isUsing()){ //logger.error("device is in use!"); continue; } //找到空闲即申请使用 currentConnectedDevice = device.getSerial(); addDeviceToUser(currentConnectedDevice); logger.info("Find free device:"+currentConnectedDevice); break; } } return currentConnectedDevice; } /* * 锁定设备,标识被某个stf的账户占用 */ private static boolean addDeviceToUser(String serial){ String reqJson = "{\"serial\": \""+serial+"\"}"; Map stfHeaderMap = new HashMap(); Map paramMap = new HashMap(); stfHeaderMap.put("Authorization","Bearer "+ACCESS_TOKEN); stfHeaderMap.put("Content-Type","application/json; charset=utf-8"); String response = httpClientAdaptor.doPost(STF_SERVICE_URL + "user/devices",stfHeaderMap,reqJson); logger.info("mq 端锁定设备响应消息:"+response); Map maps = JsonUtil.toObject(response, Map.class); // json转换为Map if (!Boolean.parseBoolean(String.valueOf(maps.get("success")))) { logger.error("Device not found"); return false; } logger.info("The device ["+serial+"]is locked successfully"); return true; } /* * 释放设备(用例用完以后,将该设备重新置为空闲) */ public static void releaseDevice(String serial) { Map paramMap = new HashMap(); Map stfHeaderMap = new HashMap(); stfHeaderMap.put("Authorization","Bearer "+stfService.getAuthToken()); String response = httpClientAdaptor.doDelete(stfService.getStfUrl() + "user/devices/"+serial,stfHeaderMap,""); logger.info("释放设备接口返回信息:"+response); }
4)用例层调用上述方法
package cn.study.testapi.base.support; import cn.study.testapi.base.enums.DrivenEnum; import cn.study.testapi.base.load.InitLoadNew; import cn.study.testapi.core.appium.server.AppiumServer; import cn.study.testapi.core.driver.driven.factory.KeyDriverFactory; import cn.study.testapi.core.driver.keydriver.AndroidKeyDriver; import cn.study.testapi.core.openstf.DeviceApi; import cn.study.testapi.utils.*; import cn.study.testapi.core.keyaction.AndroidKeyAction; import cn.study.testapi.core.entity.ComponentStep; import org.apache.log4j.Logger; import org.testng.ITestContext; import org.testng.annotations.*; import java.util.List; public class AndroidKeywordTest extends BaseKeyTest { protected static Logger logger = Logger.getLogger(AndroidKeywordTest.class); protected AndroidKeyDriver driver = null; //在testng suit之前启动appium server @Parameters({"deviceSerial"}) @BeforeSuite(alwaysRun = true) public void setUp(@Optional("deviceSerial") String deviceSerial, ITestContext context) { InitLoadNew initLoadNew = new InitLoadNew(odinEnv, taskId); // 初始化接口返回result setCaseStepList(initLoadNew.getCaseStepList()); AppiumServer.startServer();//启动appium server driver = MyAndroidKeyMobileDriver.getDriver(context); // 启动android app,根据自己的项目结构去设计此处获取driver的方式 } @Test(alwaysRun = true, dataProvider = "keyword") public void testCaseStep(List<ComponentStep> data) { //执行你的用例 } //aftersuit执行的动作,此处传一个【设备序列】参数,指明要释放的设备 @Parameters({"deviceSerial"}) @AfterSuite(alwaysRun = true) public void tearDown(@Optional("deviceSerial") String deviceSerial) { logger.info("========= tearDown ========"); DeviceApi.releaseDevice(deviceSerial);//释放移动设备 AppiumServer.stopAppiumServer();//用例执行完后停掉服务 JdbcUtil.close(); TempFileUtil.deleteOnExit(); driver.quit(); } }
代码写完以后,你就可以尝试并行执行多条android端自动化用例看看效果啦。
作者:月亮
来源:http://www.51testing.com/html/03/n-4479203.html