• 14
  • 14
分享

  一、应用场景:

  你是否有这样的困惑:通过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

  基本思路图:

1-1.png

  注:

  本文仅作为一个思路引导,真正的实践其实是根据公司现有的自动化成果来做调整的,读者可将里面对你有用的东西引入进你自己的自动化中。

  接下来将根据上述图示来写代码,本文的重点是与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上去执行。

  我们当前没有采用该种方式,考虑到移动设备需要充分利用,使用该种方式有局限性。---从当前了解到的知识来看,后续或许能找到相关解决方案。

1-2.png

  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


2021 问卷礼物图.png

  • 【留下美好印记】
    赞赏支持
登录 后发表评论
+ 关注

热门文章

    最新讲堂

      • 推荐阅读
      • 换一换
          • 随着开发模式的迭代更新,前后端分离已不是新的概念,现在大部分的项目都采用这种开发模式;当我们拿到待测试需求时,可能后端已开发完成,但前端还未完成,我们需要进行接口验证,那如何进行接口测试?就这个话题,进行一个探讨,我们先去做接口测试,从结果上来分析到底需不需要做接口测试,测试哪些内容等等。开始之前,先虚拟一个产品需求:需求描述:假设我们要做一个全新的后台项目,商品CMS管理平台(这里抛去复杂逻辑,因为需求无限拓展下去,势必大家对需求认知产生分歧,从而对测试内容产生分歧)。第一期的功能,商家用户可以在平台上进行商品的创建,编辑,删除,查询,上下架等操作;运营用户可以审核商家的商品;我们来简单描述...
            0 0 1323
            分享
          • 概述对于post请求 有几种方式。 Content-Type 实体头部用于指示资源的MIME类型 media type 。content-type是http请求头的字段。作为请求头时(post或者put),客户端告诉服务器实际发送的数据类型。对于不同的content-type 发送的数据不太一样,对于服务器端,需要如何获取数据,以及正确解析的方法也是不一样的。下面列出常用的几种 Content-Typeapplication/jsonapplication/x-www-form-urlencodedmultipart/form-datatext/plaintext/xmltext/html1....
            0 0 1383
            分享
          •   聊到自动化测试,我们做 GUI 自动化测试的过程当中,以前就只要把这个自动化做起来就好了,但随着你的用例,用的数量越来越多之后,你不单单是把一个场景自动化就可以了。因为随着你的用例变多之后,你所有的用例设置,包括你的代码的结构,都要考虑这个东西的可维护性,因为可维护性一直是 GUI 自动化测试很大的一个痛点。我们在后面的 GUI 测试过程中,就会去考虑,怎么来做分成?怎么来做基于可重用的脚本?怎么来做基于页面的对象模型?甚至到后面还有 BDD,就完全是业务,用户行为驱动的这种测试。那么,从这些概念当中,可能你已经听出来了,不管是你之前有没有接触过这些概念,你都能够发现一个很重要的信息点,自...
            0 0 797
            分享
          • 正是因为抖音直播与淘宝直播平台属性等方面的不同,淘宝直播引入“外援”拉动成绩这件事仍是一个未知数。毕竟受众究竟原因为内容买单还是单纯为商品交易本身买单仍需观察。淘宝正在积极主动扩大“社交圈”。10月24日晚间6点,曾公开宣布“退网”的罗永浩又重现在大众面前,以9.9元一箱的可口可乐、1元的三只松鼠小饼干、1元3瓶的精酿啤酒开启了淘宝“双十一”的直播首秀。此前罗永浩的“交个朋友直播间”主要在抖音开展直播。除了引入“外援”罗永浩,也有消息称,自2022年10月28日0时起,淘宝联盟商品链接将逐渐恢复在快手直播间购物车、短视频购物车、商详页等发布商品及服务链接。10月31日将正式全面恢复完成。今年3...
            0 0 742
            分享
          •   一、 功能测试  1. 点击分享按钮正确跳转分享页面,展示可分享的app,并进行分享跳转。  2. 页面默认展示自定义可分享app,有按钮支持更多app选择,过滤部分系统应用app。  3. 点击app分享,校验是否支持分享后打开链接。  4. 点击链接,其内容是否和原来的一致。  5. 是否支持取消分享。  6. 分享内容失败时,是否返回失败信息。  7. 分享内容成功,可选择留在app应用内还是返回浏览器。  8. 分享内容成功后,进入app可看到分享以链接形式展现, 展现内容为搜索标题以及搜索内容,无错别字布局合理。  9. 点击分享链接可进入浏览器展示链接内容。  二、容...
            0 0 212
            分享
      • 51testing软件测试圈微信