Appium自动化测试框架示例
来源:互联网 发布:超级优化主角几个老婆 编辑:程序博客网 时间:2024/05/16 13:06
Appium自动化测试框架
本文依赖前面那篇Appium的配置环境,讲述一个比较通用的基于Appium的自动化测试项目框架,本人Android开发,本文视角会偏向于Android平台,由于Appium是跨平台的自动化测试工具,本文讲述的项目框架依然适用于iOS平台的自动化测试方案,iOS开发可以参考,再次感谢本文参考文章的作者,谢谢你们的辛勤付出,以下是参考文章的链接,小伙伴们也可以参考:
- http://www.w2bc.com/article/182463
- http://www.w2bc.com/article/182544
正文开始
一,项目的目录结构
我们首先看一下这个测试项目的整个结构,每个目录的用途我会简要标明,然后一个一个文件讲述:
一,项目的主要代码文件
首先,我们将项目中需要用到的jar配置到pom.xml,使用maven去下载管理,以下是pom.xml的内容
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>**your groupid**</groupId> <artifactId>**your artifactid**</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>aldb</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.10</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.appium</groupId> <artifactId>java-client</artifactId> <version>4.1.2</version> <exclusions> <exclusion> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> </exclusion> </exclusions> </dependency> <!-- https://mvnrepository.com/artifact/commons-configuration/commons-configuration --> <dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.10</version> </dependency> <dependency> <groupId>net.sourceforge.jexcelapi</groupId> <artifactId>jxl</artifactId> <version>2.6.12</version> <scope>provided</scope> </dependency> <!-- Includes the Sauce JUnit helper libraries --> <dependency> <groupId>com.saucelabs</groupId> <artifactId>sauce_junit</artifactId> <version>LATEST</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/com.thoughtworks.qdox/qdox --> <dependency> <groupId>com.thoughtworks.qdox</groupId> <artifactId>qdox</artifactId> <version>1.12.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.10-FINAL</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.5</version> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>2.53.0</version> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-remote-driver</artifactId> <version>2.53.0</version> </dependency> <dependency> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty</artifactId> <version>7.0.0.pre5</version> </dependency> </dependencies> <repositories> <repository> <id>saucelabs-repository</id> <url>https://repository-saucelabs.forge.cloudbees.com/release</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.0.2</version> <executions> <execution> <goals> <goal>test-jar</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <!-- 经过测试 maven-compiler-plugin 插件版本请使用3.3,否则在jenkins上无法执行测试 --> <version>3.3</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <configuration> <argLine>-Dfile.encoding=UTF-8</argLine> <argLine>-Xms1024m -Xmx1024m -XX:PermSize=128m -XX:MaxPermSize=128m</argLine> <forkMode>never</forkMode> <suiteXmlFiles> <suiteXmlFile>testng.xml</suiteXmlFile> </suiteXmlFiles> <reportsDirectory>./result/test-report</reportsDirectory> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.android.aldb.mySql</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build></project>
配置后可能需要一定时间下载,耐心等待
因为是初次接触这个框架,我们还是用上篇的通讯录demo作为我们这个框架的学习素材,将apk包放入包目录:
准备各目录里的文件
一,base目录
下面是BasePrepare的代码:
public class BasePrepare { protected AppiumDriver<WebElement> driver; protected AppiumUtil appiumUtil; public static Logger logger = Logger.getLogger(BasePrepare.class); protected String platformName; protected String appFilePath; protected String appPackage; protected int elementTimeOut; @BeforeClass public void initTest(ITestContext context) throws MalformedURLException{ //使log4j的配置生效,以便输出日志 LogConfiguration.initLog(this.getClass().getSimpleName()); //获取platform、appFilePath、appPackage的值,这个值是从testng的配置文件获取的 if(null == context){ logger.info("null == context"); }else if(null == context.getCurrentXmlTest()){ logger.info("null == context.getCurrentXmlTest()"); } platformName = context.getCurrentXmlTest().getParameter("platformName"); appFilePath = context.getCurrentXmlTest().getParameter("appFilePath"); appPackage = context.getCurrentXmlTest().getParameter("appPackage"); elementTimeOut = Integer.valueOf(context.getCurrentXmlTest().getParameter("elementTimeOut")); appiumUtil = new AppiumUtil(); //调用SelectDriver类的selectDriver方法,生成driver对象 driver = new SelectDriver().selectDriver(context,appiumUtil); } @AfterClass public void clenTest(){ if(driver!=null){ appiumUtil.closeApp(PropertiesDataProvider.getTestData(appFilePath, appPackage));//appium模式 logger.info("请等待60秒,待下一个用例执行"); try { Thread.sleep(60000); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } // driver.quit(); //selendroid 模式 }else{ Assert.fail("driver没有获得对象,退出操作失败"); } } /** * 测试数据提供者 - 方法 * */ @DataProvider(name = "testData") public Iterator<Object[]> dataFortestMethod() throws IOException { String moduleName = null; // 模块的名字 String caseNum = null; // 用例编号 String className = this.getClass().getName(); int dotIndexNum = className.indexOf("."); // 取得第一个.的index int underlineIndexNum = className.indexOf("_"); // 取得第一个_的index if (dotIndexNum > 0) { /**这里的calssName原始值大概是这样的: * com.android.aldb.testcase.home.HomePage_002_UiCheck_Test * 那么下面这段代码className.substring(33, className.lastIndexOf("."))是什么意思?substring方法参数有两个 * 一个开始位置,一个结束位置,33表示这个字符串的第33个位置,这个位置当前字符是l,className.lastIndexOf(".")表示返回这字符串最后一个.所在 * 的位置,它是38,那么className.substring(33, className.lastIndexOf("."))可以转换成:className.substring(33, 38),最终取得的值是login, * 也就是moduleName的值 * * * */ moduleName = className.substring(26, className.lastIndexOf(".")); // 取到模块的名称 } if (underlineIndexNum > 0) { //这个分析方法和moduleName的分析方法一样 caseNum = className.substring(underlineIndexNum + 1, underlineIndexNum + 4); // 取到用例编号 } //将模块名称和用例的编号传给 ExcelDataProvider ,然后进行读取excel数据 return new ExcelDataProvider(moduleName, caseNum); }}
这里主要关注两个方法,第一个是initTest方法,里面主要的一段是
appiumUtil = new AppiumUtil(); //调用SelectDriver类的selectDriver方法,生成driver对象 driver = new SelectDriver().selectDriver(context,appiumUtil);
这个构建driver的方法主要是用配置文件里的参数来给driver做一个初始化的配置,在整个测试案例的生命周期里便可由driver来控制。第二个方法即调用driver.clossApp方法来结束整个流程,第三个方法对于我们这些初学者暂时可以不了解,其实看代码也很简单就是从Excel里读一些数据作为测试流程的数据源,所以测试案例的命名也是有一定规则的,具体可结合代码与testcase包里的测试文件名研究
二,pagehelp目录
下面是ContactsHelper的代码
public class ContactsHelper { public static Logger logger=Logger.getLogger(ContactsHelper.class); /** * @author tangjun * @param appiumUtil Appium封装对象引用 * @param byElement 要点击的元素By对象 * @description 在首页上进行点击操作 * */ public static void clickOnPage(AppiumUtil appiumUtil,By byElement){ appiumUtil.click(byElement); } /** * 输入内容,一般用在edittext控件 * @param appiumUtil * @param byElement * @param str */ public static void typeInfo(AppiumUtil appiumUtil,By byElement,String str){ appiumUtil.typeContent(byElement, str); }}
代码很简单,就是一个点击,一个输入文本,两个方法都接受By 类型作为参数,那这个自然就指向pages目录里的文件了,看下一个目录
三,pages目录
这是Contacts的代码
public class Contacts { public static final By ADD_CONTACTS = By.id("com.example.android.contactmanager:id/addContactButton");//第一页添加联系人按钮 public static final By CONTACT_NAME = By.id("com.example.android.contactmanager:id/contactNameEditText");//第二页contact name输入框 public static final By CONTACT_PHONE = By.id("com.example.android.contactmanager:id/contactPhoneEditText");//第二页contact phone输入框 public static final By CONTACT_EMAIL = By.id("com.example.android.contactmanager:id/contactEmailEditText");//第二页contact email输入框 public static final By CONTACT_PHONE_SPINNER = By.id("com.example.android.contactmanager:id/contactPhoneTypeSpinner");//第二页住宅 家,选择器 public static final By CONTACT_SPINNER_PHONE = By.xpath("//android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.FrameLayout[1]/android.widget.LinearLayout[1]/android.widget.LinearLayout[2]/android.widget.ListView[1]/android.widget.CheckedTextView[3]");//第二页号码类型是手机号 public static final By SAVE = By.id("com.example.android.contactmanager:id/contactSaveButton");//save按钮}
不多解释,很简单的从页面抓取元素的方法,这些ID或XPath的值在上一篇中有提及,使用uiautomatorViewer可快速获取到
四,testcase目录
Contacts_001_addcontact_Test代码:
public class Contacts_001_addcontact_Test extends BasePrepare { @Test public void login() { appiumUtil.time(3000); ContactsHelper.clickOnPage(appiumUtil, Contacts.ADD_CONTACTS); appiumUtil.time(3000); ContactsHelper.typeInfo(appiumUtil, Contacts.CONTACT_NAME,"楚乔"); appiumUtil.time(2000); ContactsHelper.typeInfo(appiumUtil, Contacts.CONTACT_PHONE,"18900000009"); appiumUtil.time(2000); ContactsHelper.clickOnPage(appiumUtil, Contacts.CONTACT_PHONE_SPINNER); appiumUtil.time(2000); ContactsHelper.clickOnPage(appiumUtil, Contacts.CONTACT_SPINNER_PHONE); appiumUtil.time(2000); ContactsHelper.typeInfo(appiumUtil, Contacts.CONTACT_EMAIL,"xiner@chuqiaozhuan.com"); appiumUtil.time(3000); ContactsHelper.clickOnPage(appiumUtil, Contacts.SAVE); }}
这就是一个完整的测试流程代码,这也是这个目录为什么叫testcase的原因,代码中有很多段这样的代码appiumUtil.time(2000),这个方法就是让线程睡眠一定时间让UI操作有一定的时间间隔,而不至于因为线程执行太快导致页面操作流程无法这么迅速地完成,上面代码很多次提及appiumUtil类,而到现在我们还没有看到这个类的代码,我们把这个类放在最后一个目录中,见下一个目录。
五,utils目录
AppiumUtil文件代码:
public class AppiumUtil { public static AppiumDriver<WebElement> driver; public ITestResult it; /** 定义日志输出对象 */ public static Logger logger = Logger.getLogger(AppiumUtil.class); /** * 获取driver * * @throws MalformedURLException * @throws */ public AppiumDriver<WebElement> getDriver(String url, DesiredCapabilities cap) throws MalformedURLException { driver = new AndroidDriver<WebElement>(new URL(url), cap); return driver; } public AppiumDriver<WebElement> selectDriver(AppiumUtil appiumUtil, String url, DesiredCapabilities cap) throws MalformedURLException { driver = appiumUtil.getDriver(url, cap); return driver; } /** 退出app */ public void closeApp(String appName) { driver.closeApp(); logger.info(appName + "已经关闭"); } /** 退出移动浏览器 */ public void quit() { driver.quit(); logger.info("driver已被清理"); } /** 通过By对象 去查找某个元素 */ public WebElement findElement(By by) { return driver.findElement(by); } /** * 通过By对象 去查找一组元素 * */ public List<WebElement> findElements(By by) { return driver.findElements(by); } /** 清空元素内容 */ public void clear(By byElement) { WebElement element = findElement(byElement); element.clear(); logger.info("清空元素:" + getLocatorByElement(element, ">") + "上的内容"); } /** 输入内容 */ public void typeContent(By byElement, String str) { WebElement element = findElement(byElement); element.sendKeys(str); logger.info("在元素:" + getLocatorByElement(element, ">") + "输入内容:" + str); } /** 点击 */ public void click(By byElement) { WebElement element = findElement(byElement); try { element.click(); logger.info("点击元素:" + getLocatorByElement(element, ">")); } catch (Exception e) { logger.error("点击元素:" + getLocatorByElement(element, ">") + "失败", e); Assert.fail("点击元素:" + getLocatorByElement(element, ">") + "失败", e); } } /** 查找一个元素 - appium新增的查找元素方法 */ public WebElement byFindElement(String locateWay, String locateValue) { WebElement element = null; switch (locateWay) { case "AccessibilityId": element = driver.findElementByAccessibilityId(locateValue); break; // case "AndroidUIAutomator": // element = driver.findElementByAndroidUIAutomator(locateValue); // break; case "ClassName": element = driver.findElementByClassName(locateValue); break; case "CSS": element = driver.findElementByCssSelector(locateValue); break; case "ID": element = driver.findElementById("com.yd.android.ydz:id/" + locateValue); break; case "LinkText": element = driver.findElementByLinkText(locateValue); break; case "Name": element = driver.findElementByName(locateValue); break; case "PartialLinkText": element = driver.findElementByPartialLinkText(locateValue); break; case "TagName": element = driver.findElementByTagName(locateValue); break; case "Xpath": element = driver.findElementByXPath(locateValue); break; default: logger.error("定位方式:" + locateWay + "不被支持"); Assert.fail("定位方式:" + locateWay + "不被支持"); } return element; } /** 查找一组元素 - appium新增的查找元素方法 */ public List<?> findElements(String locateWay, String locateValue) { List<?> element = null; switch (locateWay) { case "AccessibilityId": element = driver.findElementsByAccessibilityId(locateValue); break; // case "AndroidUIAutomator": // element = driver.findElementsByAndroidUIAutomator(locateValue); // break; case "ClassName": element = driver.findElementsByClassName(locateValue); break; case "CSS": element = driver.findElementsByCssSelector(locateValue); break; case "ID": element = driver.findElementsById(locateValue); break; case "LinkText": element = driver.findElementsByLinkText(locateValue); break; case "Name": element = driver.findElementsByName(locateValue); break; case "PartialLinkText": element = driver.findElementsByPartialLinkText(locateValue); break; case "TagName": element = driver.findElementsByTagName(locateValue); break; case "Xpath": element = driver.findElementsByXPath(locateValue); break; default: logger.error("定位方式:" + locateWay + "不被支持"); Assert.fail("定位方式:" + locateWay + "不被支持"); } return element; } /** 获取文本1 */ public String getText(By by) { return findElement(by).getText().trim(); } /** 获取文本2 */ public String getText(String locateWay, String locateValue) { String str = ""; switch (locateWay) { case "AccessibilityId": str = driver.findElementByAccessibilityId(locateValue).getText() .trim(); break; // case "AndroidUIAutomator": // str = // driver.findElementByAndroidUIAutomator(locateValue).getText().trim(); // break; case "ClassName": str = driver.findElementByClassName(locateValue).getText().trim(); break; case "CSS": str = driver.findElementByCssSelector(locateValue).getText().trim(); break; case "ID": str = driver.findElementById(locateValue).getText().trim(); break; case "LinkText": str = driver.findElementByLinkText(locateValue).getText().trim(); break; case "Name": str = driver.findElementByName(locateValue).getText().trim(); break; case "PartialLinkText": str = driver.findElementByPartialLinkText(locateValue).getText() .trim(); break; case "TagName": str = driver.findElementByTagName(locateValue).getText().trim(); break; case "Xpath": str = driver.findElementByXPath(locateValue).getText().trim(); break; default: logger.error("定位方式:" + locateWay + "不被支持"); Assert.fail("定位方式:" + locateWay + "不被支持"); } return str; } /** 提交 */ public void submit(By by) { WebElement element = findElement(by); try { element.submit(); } catch (Exception e) { logger.error("在元素:" + getLocatorByElement(element, ">") + "做的提交操作失败", e); Assert.fail( "在元素:" + getLocatorByElement(element, ">") + "做的提交操作失败", e); } logger.info("在元素:" + getLocatorByElement(element, ">") + "做了提交操作"); } /** * 获得webview页面的标题 * */ public String getTitle() { return driver.getTitle(); } /** * 获得元素 属性的文本 * */ public String getAttributeText(By elementLocator, String attribute) { return findElement(elementLocator).getAttribute(attribute).trim(); } /** * 在给定的时间内去查找元素,如果没找到则超时,抛出异常 * */ public void waitForElementToLoad(int elementTimeOut, final By By) { logger.info("开始查找元素[" + By + "]"); try { (new WebDriverWait(driver, elementTimeOut)) .until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver driver) { WebElement element = driver.findElement(By); return element.isDisplayed(); } }); } catch (TimeoutException e) { logger.error("超时!! " + elementTimeOut + " 秒之后还没找到元素 [" + By + "]"); Assert.fail("超时!! " + elementTimeOut + " 秒之后还没找到元素 [" + By + "]"); } logger.info("找到了元素 [" + By + "]"); } /** * 判断文本是不是和需求要求的文本一致 * **/ public void isTextCorrect(String actual, String expected) { try { Assert.assertEquals(actual, expected); } catch (AssertionError e) { logger.error("期望的结果是 [" + expected + "] 但是找到了 [" + actual + "]"); Assert.fail("期望的结果是 [" + expected + "] 但是找到了 [" + actual + "]"); } logger.info("找到了期望的结果: [" + expected + "]"); } // 时间 public static void time(int t) { try { Thread.sleep(t); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } /* 向上滑动10次 */ public static void scorllUp() { int x = driver.manage().window().getSize().width; int y = driver.manage().window().getSize().height; int during; try { for (int i = 1; i < 10; i++) { Thread.sleep(3000); driver.swipe(x / 2, y * 9 / 10, x / 2, y / 10, 500); Thread.sleep(3000); } } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } // 只滑动一次 public void scorllUp1() { int x = driver.manage().window().getSize().width; int y = driver.manage().window().getSize().height; int during; try { Thread.sleep(1000); driver.swipe(x / 2, y * 9 / 10, x / 2, y / 10, 1000); Thread.sleep(1000); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } // 向上滑动一次 public void scorllUp4() { int x = driver.manage().window().getSize().width; int y = driver.manage().window().getSize().height; int during; try { Thread.sleep(1000); driver.swipe(x / 2, y / 10, x / 2, y * 9 / 10, 1000); Thread.sleep(1000); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } public void scorllUp2() { int x = driver.manage().window().getSize().width; int y = driver.manage().window().getSize().height; int during; try { Thread.sleep(3000); driver.swipe(x / 2, y / 10, x / 2, y * 9 / 10, 500); Thread.sleep(3000); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } /** * 向左滑动 */ public void scorllUp3() { int x = driver.manage().window().getSize().width; int y = driver.manage().window().getSize().height; int during; try { driver.swipe(x * 9 / 10, y / 2, x / 10, y / 2, 2000); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } } // 判断关键字是否存在 public static boolean getPageSouce(String aaa) { return driver.getPageSource().contains(aaa); } // 关键字封装 public static WebElement byId(AppiumDriver driver, String aaa) { return byId(driver, aaa, "", ""); } /** * * @param driver * @param aaa * ("com.yd.android.ydz:id/" + ) * @param bbb * ("com.yd.android.camera:id/" + ) * @return */ public static WebElement byId(AppiumDriver driver, String aaa, String bbb, String ccc) { By by = null; if (StringUtils.isNotEmpty(aaa)) { by = By.id("com.yd.android.ydz:id/" + aaa); } else if (StringUtils.isNotEmpty(bbb)) { // 小米 by = By.id("com.android.camera:id/" + bbb); } else if (StringUtils.isNotEmpty(ccc)) { // 魅族手机 by = by.id("com.meizu.media.camera:id/" + ccc); } else { logger.info("参数错误无法初始化。。。"); } return driver.findElement(by); } /** * 暂停当前用例的执行,暂停的时间为:sleepTime * */ public void pause(int sleepTime) { if (sleepTime <= 0) { return; } try { TimeUnit.SECONDS.sleep(sleepTime); logger.info("暂停:" + sleepTime + "秒"); } catch (InterruptedException e) { e.printStackTrace(); } } /** 根据元素来获取此元素的定位值 */ public String getLocatorByElement(WebElement element, String expectText) { String text = element.toString(); String expect = null; try { expect = text.substring(text.indexOf(expectText) + 1, text.length() - 1); } catch (Exception e) { e.printStackTrace(); logger.error("failed to find the string [" + expectText + "]"); } return expect; } /** * 判断实际文本时候包含期望文本 * * @param actual * 实际文本 * @param expect * 期望文本 */ public void isContains(String actual, String expect) { try { Assert.assertTrue(actual.contains(expect)); } catch (AssertionError e) { logger.error("The [" + actual + "] is not contains [" + expect + "]"); Assert.fail("The [" + actual + "] is not contains [" + expect + "]"); } logger.info("The [" + actual + "] is contains [" + expect + "]"); } /** 跳转到webview页面 */ public void switchWebview(int index) { Set<String> contexts = driver.getContextHandles(); for (String context : contexts) { System.out.println(context); // 打印出来看看有哪些context } driver.context((String) contexts.toArray()[index]); } /** 跳转到webview页面 */ public void switchWebview(String contextName) { try { Set<String> contexts = driver.getContextHandles(); for (String context : contexts) { System.out.println(context); // 打印出来看看有哪些context } driver.context(contextName); } catch (NoSuchContextException nce) { logger.error("没有这个context:" + contextName, nce); Assert.fail("没有这个context:" + contextName, nce); } } /** * 执行JavaScript 方法 * */ public void executeJS(String js) { ((JavascriptExecutor) driver).executeScript(js); logger.info("执行JavaScript语句:[" + js + "]"); } /** * 执行JavaScript 方法和对象 用法:seleniumUtil.executeJS("arguments[0].click();", * seleniumUtil.findElementBy(MyOrdersPage.MOP_TAB_ORDERCLOSE)); * */ public void executeJS(String js, Object... args) { ((JavascriptExecutor) driver).executeScript(js, args); logger.info("执行JavaScript语句:[" + js + "]"); } /** 检查元素是不是存在 */ public boolean doesElementsExist(By byElement) { try { findElement(byElement); return true; } catch (NoSuchElementException nee) { return false; } } /** 长按操作 */ public void longPress(By by) { TouchAction tAction = new TouchAction(driver); tAction.longPress(findElement(by)).perform(); } /** 滑动 */ public void swipe(int beginX, int beginY, int endX, int endY) { TouchAction tAction = new TouchAction(driver); try { tAction.press(beginX, beginY).moveTo(endX, endY).release() .perform(); } catch (Exception e) { e.printStackTrace(); } } /** 拖拽操作 */ public void DragAndDrop(By dragElement, By dropElement) { TouchAction act = new TouchAction(driver); act.press(findElement(dragElement)).perform(); act.moveTo(findElement(dropElement)).release().perform(); } /** 放大和缩小 */ public void zoomAndPinch(int beginX, int beginY, int endX, int endY) { int scrHeight = driver.manage().window().getSize().getHeight(); int scrWidth = driver.manage().window().getSize().getWidth(); MultiTouchAction multiTouch = new MultiTouchAction(driver); TouchAction tAction0 = new TouchAction(driver); TouchAction tAction1 = new TouchAction(driver); tAction0.press(scrWidth / 2, scrHeight / 2).waitAction(1000) .moveTo(beginX, beginY).release(); tAction1.press(scrWidth / 2, scrHeight / 2 + 40).waitAction(1000) .moveTo(endX, endY).release(); multiTouch.add(tAction0).add(tAction1); multiTouch.perform(); } /** app置于后台运行 */ public void runBackgound(int runTimes) { driver.runAppInBackground(runTimes); } /** 收起键盘 */ public void hideKeyboard() { driver.hideKeyboard(); logger.info("虚拟键盘已经收起"); } /** 安装app */ public void instalApp(String appPath) { try { driver.installApp(appPath); } catch (Exception e) { logger.error("app安装失败", e); Assert.fail("app安装失败", e); } } /** app是否安装 */ public boolean isAppInstalled(String appPackage) { if (driver.isAppInstalled(appPackage)) { logger.info(appPackage + ":已经安装"); return true; } else { logger.info(appPackage + ":未安装"); return false; } } /** 页面过长时候滑动页面 window.scrollTo(左边距,上边距); */ public void scrollPage(int x, int y) { String js = "window.scrollTo(" + x + "," + y + ");"; ((JavascriptExecutor) driver).executeScript(js); } public static void coorDinate(AppiumDriver driver, int x, int y, int duration) { JavascriptExecutor js = (JavascriptExecutor) driver; HashMap<String, Integer> tapObject = new HashMap<String, Integer>(); tapObject.put("x", x); tapObject.put("y", y); tapObject.put("duration", duration); js.executeScript("mobile: tap", tapObject); } /** * 获取随机数 */ public static int getNum(int start, int end) { return (int) (Math.random() * end + start); } /** * 获取随机英文字母 */ private static char testZimu() { String chars = "abcdefghijklmnopqrstuvwxyz"; return chars.charAt((int) (Math.random() * 26)); } /** * seekbar拖动 * * @param appiumUtil */ public static void seekBar(AppiumUtil appiumUtil) { WebElement Slider = driver.findElement(By .id("com.aldb.android:id/loan_total_seekbar")); int start = Slider.getLocation().getX(); int end = start + Slider.getSize().getWidth(); int y = Slider.getLocation().getY(); TouchAction act = new TouchAction(driver); act.press(start, y).waitAction(800).moveTo(end - 1, y).release() .perform(); } /** * 切换到webview界面 */ public static void handle() { Set<String> contexts = driver.getContextHandles(); for (String context : contexts) { logger.info(context); } driver.context((String) contexts.toArray()[1]); } /** * 切换到app端 */ public static void handles() { Set<String> contexts = driver.getContextHandles(); for (String context : contexts) { logger.info(context); } driver.context((String) contexts.toArray()[0]); } public static void wait1() { try { final WebDriver wait = (WebDriver) new WebDriverWait(driver, 10); } catch (Exception e) { // TODO: handle exception e.printStackTrace(); } }}
代码注释的很清楚,可以大略了解一下,封装了一下大部分页面操作会用到的方法,主要的几个就是点击,输入,滑动等等几个操作,然后我们先看SelectDriver的代码
public class SelectDriver { //声明driver public AppiumDriver<WebElement> driver; //声明DesiredCapabilities //声明ITestContext,用于获取testng配置文件内容 public ITestContext testContext; //appium server地址 public String serverURL; //测试引擎名字 public String automationName; //测试平台名字 public String platformName; //测试平台版本号 public String platformVersion; //设备名字 public String deviceName; //android app路径 public String androidAppPath; //android app的 package public String appPackage; //android app的activity public String appActivity; //安卓独有 - 是否使用unicode键盘,使用此键盘可以输入中文字符 public boolean unicodeKeyboard; //android独有 - 是否重置键盘,如果设置了unicodeKeyboard键盘,可以将此参数设置为true,然后键盘会重置为系统默认的 public boolean resetKeyboard; //是否覆盖已有的seesssion,这个用于多用例执行,如果不设置的话,会提示前一个session还没有结束,用例就不能继续执行了 public boolean sessionOverride; //暂停的等待时间 public int sleepTime; //元素等待超时时间 public int elementTimeOut; //app文件路径,主要存储的是app的名字 public String appFilePath; //webview的名字或者叫标识符,一般以WEBVIEW开头,例如WEBVIEW_com.microsoft.bing public final static String WEBVIEW_NAME = null; //原生app的名字或者标识符,一般是NATIVE_APP public final static String NATIVEAPP_NAME = null; public String udid; //实例化本类的日志输出对象 public static Logger logger = Logger.getLogger(SelectDriver.class); public AppiumDriver<WebElement> selectDriver(ITestContext context,AppiumUtil appiumUtil) throws MalformedURLException{ //通过testng的xml文件获取serverURL参数值,并赋给 serverURL变量 serverURL = context.getCurrentXmlTest().getParameter("serverURL"); //通过testng的xml文件获取automationName参数值,并赋给 automationName变量 automationName = context.getCurrentXmlTest().getParameter("automationName"); //通过testng的xml文件获取platformName参数值,并赋给 platformName变量 platformName = context.getCurrentXmlTest().getParameter("platformName"); //通过testng的xml文件获取platformVersion参数值,并赋给 platformVersion变量 platformVersion = context.getCurrentXmlTest().getParameter("platformVersion"); //通过testng的xml文件获取deviceName参数值,并赋给 deviceName变量 deviceName = context.getCurrentXmlTest().getParameter("deviceName"); //通过testng的xml文件获取androidAppPath参数值,并赋给 androidAppPath变量 androidAppPath = context.getCurrentXmlTest().getParameter("androidAppPath"); //通过testng的xml文件获取appPackage参数值,并赋给 appPackage变量 appPackage = context.getCurrentXmlTest().getParameter("appPackage"); //通过testng的xml文件获取appActivity参数值,并赋给 appActivity变量 appActivity = context.getCurrentXmlTest().getParameter("appActivity"); //通过testng的xml文件获取unicodeKeyboard参数值,并赋给 unicodeKeyboard变量 unicodeKeyboard = Boolean.parseBoolean(context.getCurrentXmlTest().getParameter("unicodeKeyboard")); //通过testng的xml文件获取resetKeyboard参数值,并赋给 resetKeyboard变量 resetKeyboard = Boolean.parseBoolean(context.getCurrentXmlTest().getParameter("resetKeyboard")); //通过testng的xml文件获取sleepTime参数值,并赋给 sleepTime变量 sleepTime = Integer.valueOf(context.getCurrentXmlTest().getParameter("sleepTime")); //通过testng的xml文件获取elementTimeOut参数值,并赋给 elementTimeOut变量 elementTimeOut = Integer.valueOf(context.getCurrentXmlTest().getParameter("elementTimeOut")); //通过testng的xml文件获取appFilePath参数值,并赋给 appFilePath变量 appFilePath = context.getCurrentXmlTest().getParameter("appFilePath"); sessionOverride = Boolean.valueOf(context.getCurrentXmlTest().getParameter("sessionOverride")); udid=context.getCurrentXmlTest().getParameter("udid"); this.testContext = context; DesiredCapabilities cap = new DesiredCapabilities(); //告诉测试程序,当前项目目录在哪里 //设置capability,以便和appium创建session cap.setCapability("platformName",platformName); cap.setCapability("platformVersion",platformVersion); cap.setCapability("androidAppPath", androidAppPath); cap.setCapability("deviceName",deviceName); cap.setCapability("sessionOverride", sessionOverride); cap.setCapability("udid", udid); cap.setCapability("unicodeKeyboard", unicodeKeyboard); cap.setCapability("resetKeyboard", resetKeyboard); cap.setCapability("automationName",automationName); cap.setCapability("appPackage", appPackage); cap.setCapability("appActivity", appActivity); driver = appiumUtil.getDriver(serverURL, cap); testContext.setAttribute("APPIUM_DRIVER", driver); logger.info(PropertiesDataProvider.getTestData(appFilePath, appPackage)+"已经启动"); driver.manage().timeouts().implicitlyWait(elementTimeOut, TimeUnit.SECONDS); return driver; } }
从xml 的配置文件中取得所需配置参数,使用appiumUtil类构建出driver供BasePrepare调用,这个配置文件根据maven的运行方法,放在最外层的目录,命名为testng.xml代码如下:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"><suite name="魔法现金" parallel="tests" thread-count="1"> <!--server地址 --> <parameter name="serverURL" value="http://127.0.0.1:4723/wd/hub" /> <!--automationName为selendroid or appium,如果sdk版本>=17请使用appium;如果sdk版本<=17请使用selendroid --> <parameter name="automationName" value="Appium" /> <!-- 测试平台 iOS和Android --> <parameter name="platformName" value="Android" /> <!-- 平台版本 --> <parameter name="platformVersion" value="4.4" /> <!-- 设备名字,可随意起名字,但是要有意义 --> <parameter name="deviceName" value="xiaomi" /> <!-- android app路径 --> <parameter name="androidAppPath" value="res/app/android/ContactManager.apk" /> <!--app的包 --> <parameter name="appPackage" value="com.example.android.contactmanager" /> <!--app的 activity --> <parameter name="appActivity" value=".ContactManager" /> <!--是否支持unicode输入设置为true可以输入中文字符 --> <parameter name="unicodeKeyboard" value="true" /> <!-- 重置键盘输入法 --> <parameter name="resetKeyboard" value="true" /> <!--设备UDID iPhone真机使用或者android并行测试可以使用 --> <parameter name="udid" value="3DN6T16928001972" /> <!-- 设置为true之后会覆盖当前session --> <parameter name="sessionOverride" value="true" /> <!-- 进程等待1秒中的控制时间,单位是秒 --> <parameter name="sleepTime" value="1" /> <!-- 页面元素15秒不出现超时时间 --> <parameter name="elementTimeOut" value="15" /> <!-- app属性文件 --> <parameter name="appFilePath" value="res/properties/app.properties" /> <test name="添加通讯录模块" preserve-order="true"> <!-- <packages> <package name="包目录" /> </packages> --> <classes> <class name="Contacts_001_addcontact_Test所在包目录" /> </classes> </test></suite> <!-- Suite -->
这个便是程序的执行入口,遵循maven的项目管理方式,将utils包中的几个类补充一下:
ExcelDataProvider的代码:主要用途是从Excel表读取数据
/** * @author tangjun * @description: 读取Excel数据<br> * 说明:<br> * Excel放在Data文件夹下<br> * Excel命名方式:测试类名.xls<br> * Excel的sheet命名方式:测试方法名<br> * Excel第一行为Map键值<br> */public class ExcelDataProvider implements Iterator<Object[]> { private Workbook book = null; private Sheet sheet = null; private int rowNum = 0; private int currentRowNo = 0; private int columnNum = 0; private String[] columnnName; private String path = null; private InputStream inputStream = null; public static Logger logger = Logger.getLogger(ExcelDataProvider.class.getName()); /* * @description * 2个参数:<br> * moduleName - 模块的名称 * caseNum - 测试用例编号 **/ public ExcelDataProvider(String moduleName, String caseNum) { try { //文件路径 path = "data/"+moduleName+".xls"; inputStream = new FileInputStream(path); book = Workbook.getWorkbook(inputStream); // sheet = book.getSheet(methodname); sheet = book.getSheet(caseNum); // 读取第一个sheet rowNum = sheet.getRows(); // 获得该sheet的 所有行 Cell[] cell = sheet.getRow(0);// 获得第一行的所有单元格 columnNum = cell.length; // 单元格的个数 值 赋给 列数 columnnName = new String[cell.length];// 开辟 列名的大小 for (int i = 0; i < cell.length; i++) { columnnName[i] = cell[i].getContents().toString(); // 第一行的值 // 被赋予为列名 } this.currentRowNo++; } catch (FileNotFoundException e) { logger.error("没有找到指定的文件:" + "[" + path + "]"); Assert.fail("没有找到指定的文件:" + "[" + path + "]"); } catch (Exception e) { logger.error("不能读取文件: [" + path + "]",e); Assert.fail("不能读取文件: [" + path + "]"); } } /**是否还有下个内容*/ public boolean hasNext() { if (this.rowNum == 0 || this.currentRowNo >= this.rowNum) { try { inputStream.close(); book.close(); } catch (Exception e) { e.printStackTrace(); } return false; } else { // sheet下一行内容为空判定结束 if ((sheet.getRow(currentRowNo))[0].getContents().equals("")) return false; return true; } } /**返回内容*/ public Object[] next() { Cell[] c = sheet.getRow(this.currentRowNo); Map<String, String> data = new HashMap<String, String>(); for (int i = 0; i < this.columnNum; i++) { String temp = ""; try { temp = c[i].getContents().toString(); } catch (ArrayIndexOutOfBoundsException ex) { temp = ""; } data.put(this.columnnName[i], temp); } Object object[] = new Object[1]; object[0] = data; this.currentRowNo++; return object; } public void remove() { throw new UnsupportedOperationException("remove unsupported."); }}
LogConfiguration的代码:主要用途是生成每条用例的日志
/* @decription 动态生成各个模块中的每条用例的日志,运行完成用例之后请到result/log目录下查看 * */public class LogConfiguration { public static void initLog(String fileName){ //获取到模块名字 String founctionName = getFunctionName(fileName); //声明日志文件存储路径以及文件名、格式 final String logFilePath = "./result/log/"+founctionName+"/"+fileName+".log"; Properties prop = new Properties(); //配置日志输出的格式 prop.setProperty("log4j.rootLogger","info, toConsole, toFile"); prop.setProperty("log4j.appender.file.encoding","UTF-8" ); prop.setProperty("log4j.appender.toConsole","org.apache.log4j.ConsoleAppender"); prop.setProperty("log4j.appender.toConsole.Target","System.out"); prop.setProperty("log4j.appender.toConsole.layout","org.apache.log4j.PatternLayout "); prop.setProperty("log4j.appender.toConsole.layout.ConversionPattern","[%d{yyyy-MM-dd HH:mm:ss}] [%p] %m%n"); prop.setProperty("log4j.appender.toFile", "org.apache.log4j.DailyRollingFileAppender"); prop.setProperty("log4j.appender.toFile.file", "./result/log/"+founctionName+"/"+fileName+".log"); prop.setProperty("log4j.appender.toFile.append", "false"); prop.setProperty("log4j.appender.toFile.Threshold", "info"); prop.setProperty("log4j.appender.toFile.layout", "org.apache.log4j.PatternLayout"); prop.setProperty("log4j.appender.toFile.layout.ConversionPattern", "[%d{yyyy-MM-dd HH:mm:ss}] [%p] %m%n"); //使配置生效 PropertyConfigurator.configure(prop); } //**取得模块名字*/ public static String getFunctionName(String fileName){ String functionName = null; /*int firstUndelineIndex = fileName.indexOf("_"); */ functionName = fileName.substring(0, fileName.indexOf("_")); return functionName; }}
PropertiesDataProvider的代码,主要用来从.properties文件中读取相关测试数据
* @Desription 从.properties文件中读取相关测试数据<br> * * */public class PropertiesDataProvider { public static String getTestData(String configFilePath, String key) { Configuration config = null; try { config = new PropertiesConfiguration(configFilePath); } catch (ConfigurationException e) { e.printStackTrace(); } return String.valueOf(config.getProperty(key)); }}
这么多文件看起来相当的混乱,所以我把这个demo上传到了百度云,地址是链接: https://pan.baidu.com/s/1jIqTP4a 密码: sm36,还有在这补充一点的是项目是用了Intelli IDEA工具,并不是上一篇中直接在Android项目中构造一个JavaLibrary,很多地方也只是贴了代码并未详细描述,建议下载demo运行测试学习,谢谢
- Appium自动化测试框架示例
- appium自动化测试框架构建
- appium自动化测试框架构建 .
- IOS、Android自动化测试框架Appium
- 自动化测试-appium框架环境配置
- Appium 做Android 自动化测试环境搭建+示例代码运行
- Appium+Python自动化测试(二)--运行App程序示例
- APPIUM+JAVA自动化测试
- Appium IOS 自动化测试
- Appium自动化测试
- appium移动自动化测试
- Android Appium自动化测试
- Appium自动化测试Android
- Appium自动化测试-入门
- appium自动化测试流程
- appium自动化测试
- appium+python自动化测试
- Appium自动化测试实战
- RecyclerView的简单使用
- 观察者(Observer)模式
- PendingIntent
- pthread_mutex_lock实现
- oracle创建序列
- Appium自动化测试框架示例
- ASP 点击控件刷新后页面样式发生变化后台实现解决方法
- Android 官方 Training 笔记之 OpenGL ES
- java实现红黑树的插入节点
- 那些年我们用过的显示性能指标-Android
- 编JEECMS自定义栏目统计标签
- Linux相关的一些实用东西的收集,防止忘记,方便查找
- C#发起GET和POST请求的主流方法
- matlab如何将矩阵保存为图片