Appium appium 自动化

king.yu · 2015年12月21日 · 最后由 king.yu 回复于 2016年07月06日 · 2136 次阅读
本帖已被设为精华帖!

Appium 自动化实践

准备

  1. 配置 Android 开发环境;
  2. 安装 Xcode 并打开同意协议;

安装&启动

  1. 安装 homebrew

    http://brew.sh/

  2. 安装 nodejs

    https://nodejs.org/en/download/

  3. 安装 appium

    npm install -g appium

  4. 启动 appium

    appium &

实现内容

  1. UI 自动化;
  2. shell 启动;
  3. 并发之行;
  4. 报告生成;

具体实现

项目基本目录结构

  • doc :api 文档
  • output :logs、testngReport
  • src :测试代码
  • testSuits :testng.xml
  • tools :report 生成工具、selenium driver

scr 详细目录结构

-- src
-- base
-- operateFactory
-- AndroidOperate extends AppOperate
-- IosOperate extends AppOperate
-- AppOperate(abstract) :常用操作封装
-- WebOperate :常用操作封装
-- AutoTestBase :case 基类定义
-- ExecuteListener:web 日志自动打印
-- PageObjectBase :页面对象基类(通过 PageFactory 实现页面对象实例化)
-- business
-- commonMethod :针对应用的可复用方法
-- pageObject :页面对象(通过注解形式获取?--为以后自动生成该对象做铺垫)
-- testsuits :具体的 case 实现
-- util :appium 的启动、testng 的失败重跑、应用的数据的自动生成等

case 基类定义

public class AutoTestBase {
    protected static WebDriver driver;
    protected static AppOperate operateBase;
    protected static WebOperate webOperate;
    protected String port;
    protected static String date;
    protected static String time;
    protected static int timeout;
    protected static String appPackage;
    public static String platformName;
    public static String udid;
    public static String browser_name;

    public WebDriver getDriver() {
        return driver;
    }

    /**
     * Description: Prepare before Suite.
     * 测试套件运行前准备:启动driver server、driver client,配置相关参数。
     *
     * @param filePath        app的安装包路径
     * @param appName         app的安装包名
     * @param platformName    app平台 android/ios
     * @param platformVersion app运行平台的版本
     * @param deviceName      app设备名称,ios参数,例如:iPhone 6
     * @param appPackage      app包名,android参数,例如:com.xxxx.xxxx
     * @param appActivity     android参数,例如:com.xxxx.xxxx
     * @param port            app的服务端口
     * @param udid            app的设备识别符
     * @param timeout         web/app等待超时的时间,单位:秒
     * @param browser_name    web的浏览器类别,例如:chrome
     * @param remote_url      web的远程运行的url,例如:http://172.1.2.3:8888/wd/hub
     */
    @BeforeSuite(alwaysRun = true)
    @Parameters({"filePath", "appName", "platformName", "platformVersion", "deviceName", "appPackage", "appActivity", "port", "udid", "timeout", "browser_name", "remote_url"})
    public void beforeSuite(String filePath, String appName, String platformName, String platformVersion, String deviceName, String appPackage, String appActivity, String port, String udid, int timeout, String browser_name, String remote_url) {
        this.port = port;
        this.timeout = timeout;
        AutoTestBase.platformName = platformName;
        AutoTestBase.udid = udid;
        AutoTestBase.browser_name = browser_name;
        AutoTestBase.appPackage = appPackage;

        try {
            File dateFile = new File("date.txt");
            if (dateFile.exists()) {
                date = Tools.readAll("date.txt").split(":")[0].trim();
                time = Tools.readAll("date.txt").split(":")[1].trim();
                Log.logInfo("当前日期:" + date);
                Log.logInfo("当前时间:" + time);
            } else {
                Log.logInfo("date.txt 文件不存在!");
                System.exit(0);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (platformName.toLowerCase().contains("android") || platformName.toLowerCase().contains("ios")) {
            File appDir = new File(filePath);
            File app = new File(appDir, appName);

            StartAppiumServer startAppiumServer = new StartAppiumServer(port, udid);

            System.setProperty("date", date);
            System.setProperty("time", time);
            System.setProperty("deviceID", udid);

            Log.logInfo("MobileCapabilityType.filePath = " + filePath);
            Log.logInfo("MobileCapabilityType.appName = " + appName);
            Log.logInfo("MobileCapabilityType.platformName = " + platformName);
            Log.logInfo("MobileCapabilityType.platformVersion = " + platformVersion);
            Log.logInfo("MobileCapabilityType.deviceName = " + deviceName);
            Log.logInfo("MobileCapabilityType.appPackage = " + appPackage);
            Log.logInfo("MobileCapabilityType.appActivity = " + appActivity);
            Log.logInfo("MobileCapabilityType.port = " + port);
            Log.logInfo("MobileCapabilityType.udid = " + udid);

            startAppiumServer.start();

            try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            DesiredCapabilities capabilities = new DesiredCapabilities();
            capabilities.setCapability(MobileCapabilityType.APPIUM_VERSION, "1.0");
            capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, platformName);
            capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, platformVersion);
            capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath());
            capabilities.setCapability(MobileCapabilityType.UDID, udid);
            capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, deviceName);
            capabilities.setCapability(MobileCapabilityType.APP_PACKAGE, appPackage);
            capabilities.setCapability("unicodeKeyboard", "True");
            capabilities.setCapability("resetKeyboard", "True");
            capabilities.setCapability(MobileCapabilityType.APP_ACTIVITY, appActivity);

            if (platformName.toLowerCase().contains("android")) {
                String[] miUdid = StaticConfig.getMiUdid();
                capabilities.setCapability("noSign", true);

                try {
                    if (Tools.isMatch(udid, miUdid)) {
                        int tapx;
                        int tapy;
                        Map<String, String> miResolution = StaticConfig.getMiResolution();

                        tapx = Integer.parseInt(miResolution.get(udid).split("x")[0]);
                        tapy = Integer.parseInt(miResolution.get(udid).split("x")[1]);
                        HandleSafetyTips handleSafetyTipsOne = new HandleSafetyTips(udid, tapx * 7 / 10, tapy * 95 / 100);
                        HandleSafetyTips handleSafetyTipsTwo = new HandleSafetyTips(udid, tapx * 7 / 10, tapy * 7 / 10);
                        handleSafetyTipsOne.start();
                        driver = new AndroidDriver(new URL("http://127.0.0.1:" + port + "/wd/hub"), capabilities);
                        handleSafetyTipsTwo.start();
                    } else {
                        driver = new AndroidDriver(new URL("http://127.0.0.1:" + port + "/wd/hub"), capabilities);
                    }
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
                operateBase = new AndroidOperate((AndroidDriver) driver);
            } else {
                capabilities.setCapability("autoAcceptAlerts", true);
                capabilities.setCapability("language", "zh-Hans");
                try {
//                    driver = new EventFiringWebDriver(new IOSDriver(new URL("http://127.0.0.1:"+port+"/wd/hub"), capabilities)).register(executeListener);
                    driver = new IOSDriver(new URL("http://127.0.0.1:" + port + "/wd/hub"), capabilities);
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
                operateBase = new IosOperate((IOSDriver) driver);
            }
        } else {
            System.setProperty("log.info.file", "web_" + browser_name + ".log");
//            System.setProperty("log.appium.file", "appium_" + browser_name + ".log");
            Log.logInfo("CapabilityType.BROWSER_NAME = " + browser_name);
            Log.logInfo("Remote_url = " + remote_url);

            DesiredCapabilities capabilities = new DesiredCapabilities();
            capabilities.setCapability(CapabilityType.BROWSER_NAME, browser_name);

            String driverServer;
            String driverServerPath = "tools/driverServer/";
            String processName = null;
            ExecuteListener executeListener = new ExecuteListener();

            if (browser_name.toLowerCase().contains("chrome")) {
                driverServer = Tools.isWindows() ? "chromedriver.exe" : "chromedriver";
                processName = Tools.isWindows() ? "chrome.exe" : "\"Google Chrome\"";
                System.setProperty("webdriver.chrome.driver", driverServerPath + driverServer);
            } else if (browser_name.toLowerCase().contains("ie")) {
                driverServer = "IEDriverServer.exe";
                processName = "iexplore.exe";
                System.setProperty("webdriver.ie.driver", driverServerPath + driverServer);
            } else if (browser_name.toLowerCase().contains("safari")) {
                driverServer = Tools.isWindows() ? "safari.exe" : "safari";
                processName = Tools.isWindows() ? "safari.exe" : "Safari";
                System.setProperty("webdriver.safari.driver", driverServerPath + driverServer);
            } else if (browser_name.toLowerCase().contains("opera")) {
                driverServer = Tools.isWindows() ? "operadriver.exe" : "operadriver";
                processName = Tools.isWindows() ? "opera.exe" : "Opera";
                System.setProperty("webdriver.opera.driver", driverServerPath + driverServer);
            } else if (browser_name.toLowerCase().contains("firefox")) {
                processName = Tools.isWindows() ? "firefox.exe" : "Firefox";
            }

            Tools.killProcess(processName);

            try {
                if (null != remote_url && !"".equals(remote_url)) {
                    if (browser_name.toLowerCase().contains("chrome")) {
                        driver = new EventFiringWebDriver(new RemoteWebDriver(new URL(remote_url), DesiredCapabilities.chrome())).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("firefox")) {
                        driver = new EventFiringWebDriver(new RemoteWebDriver(new URL(remote_url), DesiredCapabilities.firefox())).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("ie")) {
                        driver = new EventFiringWebDriver(new RemoteWebDriver(new URL(remote_url), DesiredCapabilities.internetExplorer())).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("safari")) {
                        driver = new EventFiringWebDriver(new RemoteWebDriver(new URL(remote_url), DesiredCapabilities.safari())).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("opera")) {
                        driver = new EventFiringWebDriver(new RemoteWebDriver(new URL(remote_url), DesiredCapabilities.opera())).register(executeListener);
                    }
                } else {
                    if (browser_name.toLowerCase().contains("chrome")) {
                        driver = new EventFiringWebDriver(new ChromeDriver()).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("firefox")) {
                        driver = new EventFiringWebDriver(new FirefoxDriver()).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("ie")) {
                        driver = new EventFiringWebDriver(new InternetExplorerDriver()).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("safari")) {
                        driver = new EventFiringWebDriver(new SafariDriver()).register(executeListener);
                    } else if (browser_name.toLowerCase().contains("opera")) {
                        driver = new EventFiringWebDriver(new OperaDriver()).register(executeListener);
                    }
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            webOperate = new WebOperate(driver);
            driver.manage().timeouts().pageLoadTimeout(timeout, TimeUnit.SECONDS);
            driver.manage().timeouts().setScriptTimeout(timeout, TimeUnit.SECONDS);
        }
        driver.manage().timeouts().implicitlyWait(timeout, TimeUnit.SECONDS);
    }

    /**
     * Description: Back to home page after method.
     * app测试方法执行完成后,回到首页。
     */
    @AfterMethod(alwaysRun = true)
    public void afterMethod() {
        if (platformName.toLowerCase().contains("android") || platformName.toLowerCase().contains("ios")) {
            operateBase.backToHomePage();
        }
    }

    /**
     * Description: Driver quit after suit.
     * 测试套件执行后关闭driver。
     */
    @AfterSuite(alwaysRun = true)
    public void afterSuit() {
        if (platformName.toLowerCase().contains("android") || platformName.toLowerCase().contains("ios")) {
            ((AppiumDriver) driver).removeApp(this.appPackage);
        }
        driver.quit();
    }

    /**
     * Description: Screen Shot.
     * 实现屏幕截屏功能。
     *
     * @param fileName 截屏文件名
     * @throws IOException IO异常
     */
    public static String ScreenShot(String fileName) throws IOException {
        String filePath_screenShots;
        String filePath_testngReports = "output" + File.separator + date + File.separator + time + File.separator + udid + File.separator + "testngReports" + File.separator;
        if (platformName.toLowerCase().contains("android") || platformName.toLowerCase().contains("ios")) {
            filePath_screenShots = "output" + File.separator + date + File.separator + time + File.separator + udid + File.separator + "screenShots" + File.separator;
        } else {
            filePath_screenShots = "output" + File.separator + date + File.separator + time + File.separator + browser_name + File.separator + "screenShots" + File.separator;
        }
        FileUtils.copyFile(((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE), new File(filePath_screenShots + fileName + ".png"));
        return filePath_testngReports;
    }
}

testNg.xml
  • 每台设备配置一个脚本(脚本可设置为模版文件自动生成)
<?xml version="1.0" encoding="UTF-8"?>
<suite name="precodition">
    <parameter name="timeout" value="30"/>
    <!--platformName in  {web ,android ,ios }-->
    <parameter name="platformName" value="web"/>
    <!--APP PARAM-->
    <parameter name="appName" value=""/>
    <parameter name="filePath" value=""/>
    <parameter name="platformVersion" value=""/>
    <parameter name="deviceName" value=""/>
    <parameter name="appPackage" value=""/>
    <parameter name="appActivity" value=""/>
    <parameter name="port" value=""/>
    <parameter name="udid" value=""/>
    <!--WEB PARAM-->
    <parameter name = "browser_name" value = "chrome"/>
    <parameter name = "remote_url" value = ""/>

    <test name="Web_Tests">
        <groups>
            <define name="p0">
                <include name="p0" />
            </define>
            <define name="p1">
                <include name="p1" />
            </define>
            <define name="p2">
                <include name="p2" />
            </define>
            <run>
                <include name="p0" />
                <!--<exclude name="xxxx" />-->
            </run>
        </groups>
        <classes>
            <class name="com.xxxx.xxxx.testsuits.testcases.web.testExamlpe"/>
        </classes>
    </test>

    <listeners>
        <listener class-name="com.xxxx.xxxx.util.testng.TestResultListener" />
        <listener class-name="com.xxxx.xxxx.util.testng.RetryListener" />
    </listeners>
</suite>

邮件报告模版
<html>
<head>
    <meta charset="utf-8" />
    <style>
        body {
            font-family: 华文细黑,Calibri,sans-serif;
            font-size: 16px;
        }
        table.stats-table {
            color:black;
            border-width: 1px;
            border-spacing: 2px;
            border-style: outset;
            border-color: gray;
            border-collapse: collapse;
            background-color: white;
        }
        table.stats-table th {
            color:black;
            border-width: 1px;
            padding: 5px;
            border-style: inset;
            border-color: gray;
            background-color: #66CCEE;
            -moz-border-radius: ;
        }
        table.stats-table td {
            color:black;
            text-align: center;
            border-width: 1px;
            padding: 5px;
            border-style: inset;
            border-color: gray;
            background-color: white;
            -moz-border-radius: ;
        }
    </style>
</head>
<body>
<table class="stats-table">
    <tbody>
    <tr>
        <th id="stats-header-scenarios" colspan="6">Test Report</th>
    </tr>
    <tr>
        <th>tests</th>
        <th style="background-color:#C5D88A;">passes</th>
        <th style="background-color:#D88A8A;">failures</th>
        <th style="background-color:#D88A8A;">errors</th>
        <th style="background-color:#2DEAEC;">skipped</th>
        <th style="background-color:#2DEAEC;">time</th>
    </tr>
    <tr>
        <th style="background-color:yellow;"> #testNgResults.Total </th>
        <th style="background-color:yellow;"> #testNgResults.Passed </th>
        <th style="background-color:yellow;"> #testNgResults.Failed </th>
        <th style="background-color:yellow;"> #testNgResults.Errors </th>
        <th style="background-color:yellow;"> #testNgResults.Skipped </th>
        <th style="background-color:yellow;"> #testNgResults.Time </th>
    </tr>

    <tr>
        <th colspan="6">Failed tests </th>
    </tr>
    <tr>
        <th colspan="2" style="background-color:#2DEAEC;">testName</th>
        <th colspan="2" style="background-color:#2DEAEC;">className</th>
        <th colspan="2" style="background-color:#2DEAEC;">methodName</th>
    </tr>

    </tbody>
</table>
</body>
</html>
报告总览模版

case 启动脚本
  • 脚本由 java 多线程去启动.(踩的坑:通过设置 suitethreadpoolsize 无法并行运行)
#!/bin/bash
date=`cat date.txt |cut -d : -f1`
time=`cat date.txt |cut -d : -f2`
currentPath=`pwd`

java -classpath "target/test-classes/" -Djava.ext.dirs=lib org.testng.TestNG -suitethreadpoolsize 1 TestSuits/APP_*_{devices}_*.xml -d output/${date}/${time}/{udid}/testngReports

cd tools/testngReport
# testng 美化插件
ant transform -Din=${currentPath}/output/${date}/${time}/{udid}/testngReports/testng-results.xml -Dout=${currentPath}/output/${date}/${time}/{udid}/testngReports/index_xslt.html -Dexpression=${currentPath}/output/${date}/${time}/{udid}/testngReports/

地址

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 35 条回复 时间 点赞

补充一句,node 需要使用 homebrew 来进行安装,不然独立从 nodejs 上面下载的话只能用 sudo 的命令进行安装 appium 但是不能直接使用命令启动,而通过 homebrew 安装即可

收获颇多,谢谢分享!

楼主,用的什么软件的报告甘特图,看起来很好看

#3 楼 @284772894 highcharts

楼主,感谢分享,获益匪浅!
appium+ruby 如何生成测试报告?

#5 楼 @tongxiaoxing

可以尝试用 cucumber 框架去做 _^ 报告还是挺美观的 。

楼主,是不是目前 appium 框架,用 Ruby 脚本还没有较好的生成测试报告的工具?
谢谢~

#7 楼 @tongxiaoxing

我用的是 testng+appium(java client) 你可以使用 cucumber(BDD)+appium 去做,或者其他的 ruby 测试框架,appium 只是 ui 自动化的封装。

感谢楼主分享,请问有 github 链接吗?谢谢

#9 楼 @zchunley 有需要的话可以提供_^

同求分享 谢谢

#13 楼 @kinget007 多谢,在 java 的基础有点淡忘了,多谢提供,下来好好学习~!

谢谢分享

@kinget007 感谢分享~
date.txt 是干什么用的呢?

匿名 #22 · 2016年01月07日

厉害

报告里的线是什么数据

好例子

仿佛打开了个新世界的大门。。。

#23 楼 @dongdong 用例的 total,pass failure skip

谢谢分享

@kinget007 大神你好 签入 github 上的代码再看了,遇到些问题 想咨询下 能否给个联系 QQ 呢

#28 楼 @harsayer 806833417

30楼 已删除

你好,请问为什么执行完 BatchRunAppUI.jar 后是在这样呢?
android 测试设备列表:[]
ios 测试设备列表:[]
没有检测到设备?
运行 run.sh 还有会报一个 com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

#31 楼 @yxyang
加 QQ 806833417

—— 来自 TesterHome 官方 安卓客户端

java -classpath "target/test-classes/" -Djava.ext.dirs=lib org.testng.TestNG -suitethreadpoolsize 1 TestSuits/APP_{devices}.xml -d output/${date}/${time}/{udid}/testngReports
testng 环境我其实配置好了,运行这一行会出找不到或无法加载主类 org.testng.TestNG。

这个例子下好后 怎么报错信息这么多!?

#34 楼 @zhuzhuhoye
以 maven 格式导入,如果是直接 open 项目的 那右击 pom.xml 文件 选择 reimport

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册