基于Appium的手机H5应用UI自动化实践(一)
目录
简单说明
首先介绍一下我这里提到的手机H5页面是什么东西,做APP测试的同学应该都知道手机APP里面也嵌入了一些H5页面,就是通过APP内嵌的webview组件去承载的页面。但是我这里做的手机H5的测试不是APP内嵌的H5,而是通过手机浏览器打开的页面,也就是普通的网页。比如我们下面的实例中用到百度的手机网页版 http://m.baidu.com
之所以这么啰嗦的区分这两种H5是因为用appium做UI自动化,测试APP里面的H5和测试手机浏览器打开的H5的操作流程上是有所区别的。比如要测试APP内嵌的H5需要先操作appium启动APP,然后通过context切到webview模式,才能操作H5页面,但是如果测试手机网页的话就比较简单了,设置好浏览器比如选择Chrome,直接访问网址就好了。如果抛开外面的框架不说,单纯两种H5页面的测试上是没有太大区别的。
准备测试环境
- 最重要也是最基本的安装appium,appium的安装这里就不过过多的介绍了,可参加这位仁兄的博客:https://blog.csdn.net/liuchunming033/article/details/51544633
- 安装JDK,Maven ,开发语言我选择的是Java,因为比较熟。也可以选择Python等,因为appium支持多语言
- 安装adb,需要使用它去连接测试机或者模拟器,如果使用iPhone的话还需要安装Xcode
搭建测试框架
测试库的构建就是按照上图的框架来操作的,为了简单起见,没有引入过多的复杂功能,比如测试用例的管理,测试日志的管理等,只有简单的测试用例。我们先按照上图简单介绍一下各个模块。
- BaseDriver主要作用就是负责与测试机或者模拟器之间建立连接
- PageObjects这个模块其实是一种成熟的UI页面测试模式,就是把每个单一的页面写到一个类里面,包括页面的元素,页面的操作等等,这样便于维护。我这里把这个模块又细分成了两个模块,一个是页面模块另一个是页面元素模块,考虑到页面元素太多为了便于管理和升级改动我单独把元素从页面里面拉出来,这个后面会详细介绍
- PageNavigator这个模块功能很简单,其实就是个页面池子,测试中用到的页面对象会写入池子,用的时候再取出来,这个模块其实是为了方便测试用例级别操作页面更简洁一些,
- TestCases这个模块就不用过多的介绍了,就是测试用例
- GlobalVars & TestData这模块用户存放测试中用到的变量和测试数据
对应代码层面是这个样子的
各模块详细介绍
BaseDriver
废话不多说了先上代码,通过代码做展开说明
package testdriver;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.ios.IOSDriver;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import testdata.GlobalVars;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
public class BaseDriver {
public static WebDriver driver;
/**
* 启动driver
* @return
*/
public void startDriver() throws MalformedURLException {
/**
* 初始化appium webdriver
*/
initializeTestData();
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("deviceName", GlobalVars.DEVICE_NAME);
capabilities.setCapability("browserName", GlobalVars.BROWER_NAME);
capabilities.setCapability("platformVersion", GlobalVars.PLATFORM_VERSION);
capabilities.setCapability("platformName", GlobalVars.PLATFORM_NAME );
capabilities.setCapability("noReset", GlobalVars.NO_RESET);
capabilities.setCapability("unicodeKeyboard", true);
capabilities.setCapability("resetKeyboard", true);
if (GlobalVars.PLATFORM_NAME.contains("iOS")){
capabilities.setCapability("udid", GlobalVars.UDID);
driver = new IOSDriver(new URL(GlobalVars.APPIUM_SERVER), capabilities);
}else{
driver = new AndroidDriver(new URL(GlobalVars.APPIUM_SERVER), capabilities);
}
driver.get(GlobalVars.TEST_URL);
}
/**
* 退出driver
*/
public void stopDriver(){
driver.quit();
}
/**
* 初始化测试环境数据
*/
private void initializeTestData(){
Properties prop = new Properties();
try{
//读取属性文件a.properties
InputStream in = new BufferedInputStream(new FileInputStream("MyConfig.prop"));
///加载属性列表
prop.load(in);
GlobalVars.DEVICE_NAME = prop.getProperty("device_name");
GlobalVars.PLATFORM_NAME = prop.getProperty("platform_name");
GlobalVars.PLATFORM_VERSION = prop.getProperty("platform_version");
GlobalVars.UDID = prop.getProperty("udid");
GlobalVars.BROWER_NAME = prop.getProperty("brower_name");
GlobalVars.APPIUM_SERVER = prop.getProperty("appium_server");
GlobalVars.NO_RESET = prop.getProperty("no_reset").equals("true")?true:false;
GlobalVars.TEST_URL = prop.getProperty("test_url");
in.close();
}
catch(Exception e){
System.out.println(e);
}
}
}
这个模块的主要作用就是连接真机,所需的参数我没有直接写死在代码里面而是通过配置文件读取到程序里面,这个是为了后面部署到jenkins上面方便配置,具体可以参见工程里面的MyConfig.prop文件。
device_name=i
platform_name=Android
platform_version=7.0
brower_name=chrome
udid=827dc51fd4adcc5234164e581f63bcba11547923
appium_server=http://127.0.0.1:4723/wd/hub
no_reset=true
test_url=http://m.baidu.com
下面以Android机为例介绍一下连接手机过程中的几个步骤和重点。
1. 连接测试机(两种方式)
一种是adb connect ip:port(如adb connect 172.18.93.115:7449)的方式通过网络连接,在prop文件里只配置
- platform_name=Android
- platform_version=7.0
- device_name=随便写,
另一种是通过USB本地连接的话还需要配置 device_name=手机的***(首先手机USB数据模式连接本机,命令行输入adb devices就会看到当前手机的***)
2. 选择浏览器,我一般选择chrome,如果没有安装Chrome的话 直接写broswer_name=broswer的话就是手机默认浏览器
3. appium_server的值是默认的直接照抄就行了
4. udid是iOS才会用到,Android的话随便写
5. no_reset=true 意思就是每次不要跑一个用例就重启一下浏览器
6. test_url就是要测试的网页地址
其实以上参数都是appium webdriver启动的参数,还有很多参数大家可以查阅appium的官方文档。
如果以上都配置对了,手机已经连接好了,下面就是启动appium server, 我这里没有安装appium的界面工具,所以都是命令行直接输入appium 回车就OK了
PageObjects
介绍框架的时候提到过,我这里把PageObjects模式细化为两个模块,对应代码中的pages和pageselements两个文件夹
通过代码来了解一下这两个模块的关系以及实现方式。
PageHomeElms主要功能是对页面元素的初始化并提供页面元素的对象。 初始化元素用到了两种方式,
首先该类初始化的时候回通过PageFactory模式定位到当前页面已经加载出来并且被@FindBy标记的元素,
如果元素是后加载的或者PageFactory没有定位到的话后面还可以通过webdriver的findElement方法定位
PageHome的主要功能是向testcase提供页面的各种操作,比如页面的滑动,元素的点击,是否可见等。
该类会直接饮用PageHomeElms里面初始化好的元素进行操作
下面具体以百度的H5页面,完成一个搜索功能为例看一下代码的实现
package pageobjects.pages;
import pageobjects.baseclass.PageObjectBase;
import pageobjects.pageselements.PageHomeElms;
public class PageHome extends PageObjectBase {
private PageHomeElms homePageElms = new PageHomeElms();
/**
* home页面的无参构造函数,初始化一下页面元素
*/
public PageHome(){
//页面元素初始化,只订单当前页面已经加载完的元素
homePageElms = homePageElms.initElements();
}
public PageHome inputSearchWord(String keyword){
input(homePageElms.getSearchBox(),keyword);
return this;
}
public PageHome tapSearchBtn(){
tap(homePageElms.getIndexBtn());
return this;
}
}
package pageobjects.pageselements;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import pageobjects.baseclass.ElementsBase;
/**
* Home页面的元素
*/
public class PageHomeElms extends ElementsBase {
//搜索输入框
public static final String xpathSearchBox = "//*[@id=\"index-kw\"]";
public static final By locatorSearchBox = new By.ByXPath(xpathSearchBox);
@FindBy(xpath = xpathSearchBox)
private WebElement searchBox;
//"百度一下"button,以下列举了三种xpath方式获取元素
//public static final String xpathIndexBtn = "//*[@class=\"se-bn\"]";
//public static final String xpathIndexBtn = "//*[@id=\"index-bn\"]";
public static final String xpathIndexBtn = "//button[contains(text(),'百度一下')]";
public static final By locatorIndexBtn = new By.ByXPath(xpathIndexBtn);
@FindBy(xpath = xpathIndexBtn)
private WebElement indexBtn;
/**
* 初始化元素
* @return 当前页面对象
*/
public PageHomeElms initElements(){
//通过PageFactory工具加载当前页面已有元素,未加载的元素后续可以通过定位器单独找到
return locatePgElms(PageHomeElms.class);
}
/**
* 获取元素 SearchBox, 如果PageFactory方式没有定位到该元素,
* 则通过传统的方式获取
* @return
*/
public WebElement getSearchBox(){
if (searchBox == null){
return locateElement(locatorSearchBox);
}else{
return searchBox;
}
}
/**
* 获取元素 IndexBtn, 如果PageFactory方式没有定位到该元素,
* 则通过传统的方式获取
* @return
*/
public WebElement getIndexBtn(){
if (indexBtn == null){
return locateElement(locatorIndexBtn);
}else{
return indexBtn;
}
}
}
Chrome浏览器的开发者工具提供了获取xpath的方法,不过个人感觉这种方式获取到的xpath一般比较啰嗦,自己可以根据xpath的规范自己去优化
除此之外通过代码可见,PageHome类和PageHomeElements类还各自继承了对应的父类。PageObjectBase和ElementBase.
这两个类主要提供页面和元素的通用操作
package pageobjects.baseclass;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebElement;
import testdriver.BaseDriver;
import utils.Utils;
/**
* 页面及页面元素的常用操作
*/
public class PageObjectBase {
/**
* 向元素输入文案
* @param elm
* @param txt
*/
public void input(WebElement elm, String txt){
elm.sendKeys(txt);
}
/**
* 点击元素
* @param elm
*/
public void tap(WebElement elm){
elm.click();
}
/**
* 判断元素是否展示
* @param elm
* @return
*/
public boolean isDisplayed(WebElement elm){
if ( elm != null){
return true;
}else{
return false;
}
}
/**
* 移动到页面最底部
*/
public static void swipToBottom(){
((JavascriptExecutor) BaseDriver.driver).executeScript("window.scrollTo(0, document.body.scrollHeight)");
Utils.stopThreadTimeSec(2);
}
/**
* 移动到页面最底部
*/
public static void swipToTop(){
((JavascriptExecutor) BaseDriver.driver).executeScript("window.scrollTo(0, 0)");
Utils.stopThreadTimeSec(2);
}
}
package pageobjects.baseclass;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import testdriver.BaseDriver;
import utils.Utils;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ElementsBase {
/**
* 使用PageFactory模式定位当前页面已经加载完的元素
* @param pageClass 页面类
* @param <T> 页面对象
* @return
*/
public <T> T locatePgElms(Class<T> pageClass){
return PageFactory.initElements(BaseDriver.driver, pageClass);
}
public WebElement locateElement(By locator){
return findElement(locator,5);
}
public List<WebElement> locateElements(By locator){
return findElements(locator,5);
}
/**
* 定位到单个元素
* @param locator 元素定位器
* @param waitTime 等待元素的时间,超过此时间还未定位到即为未找到,返回空
* @return 元素对象
*/
public WebElement findElement(final By locator, int waitTime) {
WebElement elm = null;
for(int i = 0; i < waitTime; i++){
try{
elm = BaseDriver.driver.findElement(locator);
break;
}catch (Exception ex){
BaseDriver.driver.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS);
}
}
return (WebElement) elm;
}
/**
* 定位到一组元素
* @param locator 元素定位器
* @param waitTime 等待元素的时间,超过此时间还未定位到即为未找到,返回空
* @return 一组元素对象
*/
public static List<WebElement> findElements(final By locator, int waitTime) {
List<WebElement> elms = null;
for(int i = 0; i < waitTime; i++){
try{
elms = BaseDriver.driver.findElements(locator);
break;
}catch (Exception ex){
BaseDriver.driver.manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS);
}
}
return elms;
}
/**
* 滑动元素使其可见
* @param elm
*/
public static void swipToVisible(WebElement elm){
((JavascriptExecutor) BaseDriver.driver).executeScript("arguments[0].scrollIntoView(true);", elm);
Utils.stopThreadTimeSec(2);
}
}
PageNavigator
从首页进入搜索结果页时,当前操作的页面对象要多相应的切换,当从搜索结果页返回首页时,还需要切回之前的首页对象,所以这个过程中就需要一个页面池子来保存一些页面对象
package pageobjects.baseclass;
import java.util.ArrayList;
import java.util.List;
public class PgNavigator {
//保存已经实例化的页面,用于在测试中页面之间的切换
public static List<Object> pagePool = new ArrayList<Object>();
/**
* 页面跳转的导航器,如果所需页面没有实例化则实例化后存入页面池
* 如果所需页面已经存在则直接从页面池中取出并返回
* @param pageClass 页面类
* @param <T> 页面实例
* @return
*/
public static <T> T navigt2Pg(Class<T> pageClass) {
try{
for (int i=0; i<pagePool.size(); i++){
if (pagePool.get(i).getClass().equals(pageClass)){
return (T) pagePool.get(i);
}
}
pagePool.add(pageClass.newInstance());
return (T) pagePool.get(pagePool.size()-1);
}catch (Exception e){
System.out.print(e.getMessage());
return null;
}
}
}
TestCases
简单起见,代码中只简单罗列了page的引用,并没有对操作结果多断言等后续操作
package testcases;
import org.testng.annotations.Test;
import pageobjects.pages.PageHome;
public class TCHomePg extends TestCaseBase{
@Test
public void testCase1(){
pgNavigator.navigt2Pg(PageHome.class).inputSearchWord("appium").tapSearchBtn();
//TODO 实际测试用例需要有断言
}
}
package testcases;
import org.testng.annotations.*;
import pageobjects.baseclass.PgNavigator;
import testdriver.BaseDriver;
import java.net.MalformedURLException;
public class TestCaseBase {
public BaseDriver baseDriver = new BaseDriver();
public PgNavigator pgNavigator;
@BeforeSuite
public void beforeSuit() throws MalformedURLException {
//整个测试库运行期间只启动一次driver
baseDriver.startDriver();
}
@BeforeClass
public void beforeClass(){
}
@BeforeMethod
public void beforeTest(){
//每个测试方法运行前清空一下页面池
pgNavigator = new PgNavigator();
}
@AfterMethod
public void afterTest(){
}
@AfterClass
public void afterClass(){
}
@AfterSuite
public void afterSuit(){
baseDriver.stopDriver();
}
}
下面补充一下GlobalVars文件和工具类文件的源码
package testdata;
public class GlobalVars {
public static String DEVICE_NAME;
public static String PLATFORM_NAME;
public static String PLATFORM_VERSION;
public static String BROWER_NAME;
public static String APPIUM_SERVER;
public static String UDID;
public static boolean NO_RESET;
public static String TEST_URL;
}
package utils;
public class Utils {
/**
* 系统延时等待
* @param times
*/
public static void stopThreadTimeSec(int times){
try{
Thread.sleep(times*1000);
}catch (Exception ex){
System.out.print(ex.getMessage());
}
}
}
结束语
时间比较仓促,有些地方写的不够细致,另外这套简单的测试库是本人在实际工作中总结的,之前也没有阅读前辈们的解决方案,所以这套流程还存在很多待改进的地方,如果有写的不够完善的地方还请多指教,大家共同学习与进步!后续有改善的地方我会随时更新。
谢谢!
上一篇: PV操作典型——哲学家进餐问题
下一篇: face_recognition人脸识别