Appium自动化测试框架
本文依赖前面那篇Appium的配置环境,讲述一个比较通用的基于Appium的自动化测试项目框架,本人Android开发,本文视角会偏向于Android平台,由于Appium是跨平台的自动化测试工具,本文讲述的项目框架依然适用于iOS平台的自动化测试方案,iOS开发可以参考,再次感谢本文参考文章的作者,谢谢你们的辛勤付出,以下是参考文章的链接,小伙伴们也可以参考:
正文开始
一,项目的目录结构
我们首先看一下这个测试项目的整个结构,每个目录的用途我会简要标明,然后一个一个文件讲述:
一,项目的主要代码文件
首先,我们将项目中需要用到的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运行测试学习,谢谢