前言
做Android端功能自動化已有2年多的時間了,使用過的功能自動化框架有Robotium、Uiautomator、Appium。最近研究自動化case復用的方案,調研了Appium的自動化框架,並將其應用到銀行一賬通的標版中,本文詳細介紹基於Appium的Android功能自動化實戰經驗。主要包括以下幾方面內容:
- Appium框架原理介紹
- Appium框架常用API介紹
- 基於Appium框架的自動化開發環境搭建
- 自動化case開發及分層結構設計
- 自動化測試用例書寫規范及注意事項
- 功能自動化接入持續集成方案
Android常用功能自動化框架比較
下表給出Robotium、Uiautomator、Appium三種框架的比較。
了解各個自動化框架的特性,結合產品自身特點,選取一個合適的框架尤為重要。

Appium框架原理介紹
Appium 是一個開源、跨平台的自動化測試工具,用於測試原生和輕量移動應用,支持iOS, Android 和 FirefoxOS 平台。Appium 驅動蘋果的 UIAutomation 庫和 Android 的 UiAutomator 框架,使用 Selenium 的 WebDriver JSON 協議。下圖是Appium在Android端的原理架構圖

從圖中可以看出,Appium Client是對webdriver原生api的一些擴展和封裝,它配合原生的webdriver來使用,二者缺一不可。Appium Server有兩個主要功能:首先它作為一個http服務器端,接收從Appium Client發送過來的命令(可以認為是case中的具體操作)。其次,它作為bootstrap客戶端,在接收Client的命令后,通過socket方式,把這些命令發給目標android機器的bootstrap來驅動Uiautomator執行自動化操作。關於Appium的工作原理,網上資料很多,不再詳細介紹,可參考如下鏈接
http://www.2cto.com/kf/201410/347851.html
http://www.blogjava.net/qileilove/archive/2014/12/23/421659.html
Appium框架常用API介紹
Appium的API包含在Appium Client中,下表給出不同語言平台對應的Client下載地址

下表給出Java平台的常用API,其他API可自行百度或查看Client源碼

Appium元素定位和操作的api是分開的,這點與robotium框架不同,與Google新推出的iOS端功能自動化框架EarlGrey是類似的。關於元素定位的api需要注意:當成功定位到元素時,返回WebElement對象,但是若不能定位到元素,此api直接報錯,而不是返回NULL。這對“判斷某個控件是否存在”這樣的常用操作經常容易出錯,比如用如下代碼判斷登錄按鈕是否存在。
public boolean isLoginButtonShow(){ WebElement wl = DriverManager.getInstance().findElementById(packagename + ":id/login_input_account"); return wl.isDisplayed(); }
當登錄按鈕存在時,返回true,但若登錄按鈕不存在時,以上代碼並不會返回false,而是在第2行直接崩潰。可通過try catch捕獲異常,修改如下:
public boolean isLoginButtonShow(){ try{ WebElement wl = DriverManager.getInstance().findElementById(packagename + ":id/login_input_account"); return wl.isDisplayed(); }catch (Exception e){ return false; } }
基於Appium框架的自動化開發環境搭建
萬事開頭難,自動化開發環境的搭建會比較麻煩。以下詳細講解如何在mac os操作系統下,搭建基於Appium的自動化開發環境。
1、Android開發環境搭建(JDK/SDK/AndroidStudio)請自行百度,所需安裝包可在如下網頁中下載:
http://tools.android-studio.org
Appium for mac安裝可參考以下連接:
http://www.cnblogs.com/oscarxie/p/3894559.html
其中appium 的安裝,建議使用dmg安裝,點擊下載安裝包。“npm install -g appium”命令行安裝親測一直報錯,翻牆了也報錯。
2、AndroidStudio中新建android工程AppDemo。
3、創建java module(appium):new—>new module—>java library,此java工程用來開發自動化case。
4、安裝Appium Client:創建java module (selenium),libs目錄中導入相關selenium包,點擊下載selenium壓縮包
5、appium module添加selenium依賴:file—>project structure—>modelues選中appium—>dependencies添加selenium module。
6、appium module中,新建java class,開始開發自動化case。至此AppDemo工程目錄結構如下所示:

7、啟動appium server(命令行或者圖形界面啟動),注意,啟動時Android Settings中請勾選No Reset選項,可防止每次執行case時,都重新安裝app,如下所示。

8、case執行,可通過IDE和腳本兩種方式執行case
IDE方式:case名右擊—>run
腳本方式:auto_run.sh,見下方
#!/bin/bash #auto_run.sh source ~/.bash_profile cd ./AppDemo gradle clean gradle build export CLASSPATH=$APPIUM_HOME/java-client-3.3.0.jar:$APPIUM_HOME/selenium-java-2.48.2-srcs.jar:$APPIUM_HOME/selenium-java-2.48.2.jar:$APPIUM_HOME/junit-4.12.jar:$APPIUM_HOME/hamcrest-core-1.3.jar:$APPIUM_HOME/libs/* cd ./appium java -classpath $CLASSPATH:./build/libs/appium.jar org.junit.runner.JUnitCore com.incito.appiumdemo.cases.PersonalCente
其中CLASSPATH需要設置為Appium Client所在路徑,
com.incito.appiumdemo.cases.PersonalCenter為case所在類,腳本的執行方式有利於自動化接入持續集成和平台化。
至此基於Appium的自動化開發環境搭建完成,下一節介紹如何開發自動化case及case的分層結構設計。
基於Appium的自動化case開發及case分層結構設計
首先為每條case創建一個公共的基類AppiumTestBase,內含setup和teardown兩個方法,以后每條case繼承該基類即可。代碼如下:
public class AppiumTestBase { public WebDriverWait webwait; private AndroidDriver driver; @Before public void setUp() throws Exception { File classpathRoot = new File(System.getProperty("user.dir")); File appDir = new File(classpathRoot, "apps"); File app = new File(appDir, Config.CURRENT_BANK); DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setCapability("deviceName",Config.DEVICE_NAME); capabilities.setCapability(CapabilityType.BROWSER_NAME, ""); capabilities.setCapability(CapabilityType.VERSION, "5.0.1"); capabilities.setCapability("platformName", "android"); capabilities.setCapability("app", app.getAbsolutePath()); capabilities.setCapability("udid", Config.DEVICE_NAME);//adb devices獲得的值 driver = new AndroidDriver(new URL("http://127.0.0.1:"+Config.APPIUM_PORT+"/wd/hub"), capabilities); webwait = new WebDriverWait(driver,10); DriverManager.init(driver); DriverManager.initWebWait(webwait); driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); } @After public void tearDown() throws Exception { driver.quit(); }
setup操作包含導入待測應用apk包,設置與Appium Server連接所需參數,並初始化AndroidDriver對象。以標版登錄case為例,其需要繼承AppiumTestBase,代碼如下:
package com.incito.appiumdemo; import org.junit.After; import org.junit.Before; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.support.ui.WebDriverWait; import java.io.File; import java.net.URL; import java.util.concurrent.TimeUnit; import io.appium.java_client.android.AndroidDriver; public class AccountLogin extends AppiumTestBase{ /** * case 編號 AccountLogin_001 * ****@throws ****Exception */ @Test public void testAccountLogin() throws Exception { Account.*getInstance*().gotoLoginPage(); Account.*getInstance*().doLogin(); } /** * case 編號 AccountLogin_002 * ****@throws ****Exception */ @Test public void testLoginout() throws Exception { Account.*getInstance*().gotoGestureLoginPage(); Account.*getInstance*().doGestureLogin(Config.*CURRENT_USERNAME*); Personal.*getInstance*().doLogout(); }
testAccountLogin Case中,執行了兩步操作:進入登錄頁面;執行登錄操作。這兩步操作都封裝在Account類里面。由此引入自動化case的分層設計框架,如下圖:

自動化開發過程中,經常遇到的一個問題是,隨着產品的不斷更新迭代,APP的UI會不斷發生變化,自動化如何去應對這樣的變化,如何降低其維護代價。case分層設計主要是基於自動化可維護性的考慮,可維護性是功能自動化最重要的評價指標之一,其直接決定了自動化是否能開展下去。試想case數量達到一定程度時,若沒有采用封裝、分層的設計思路,極有可能出現“牽一發而動全身”的問題。下圖是標版自動化case的分層目錄圖。

下面以登錄case為例,詳細了解其分層調用關系。登錄case在cases層的AccountLogin類中,其代碼上面已經展示,回到testAccountLogin Case的操作,第二步操作為Account.getInstance().doLogin();即在登錄頁面執行登錄操作,其封裝在business層的Account類中,代碼片段如下:
public void doLogin(){ LoginUiAction.*getInstance*().enterUsername(Config.*CURRENT_USERNAME*); LoginUiAction.*getInstance*().enterPassword(Config.*CURRENT_PASSWORD*); LoginUiAction.*getInstance*().clickLogin(); HomeUiAction.*getInstance*().clickOnCancelGesture(); Assert.*assertTrue*(LoginUiAction.*getInstance*().isLogin()); }
doLogin()方法中執行了“輸入用戶名;輸入密碼;點擊登錄按鈕;判斷登錄是否成功”操作,顯然這幾步操作都在UI層LoginUiAction類中。我們再查LoginUiAction.getInstance().
enterUsername()方法中做了什么,代碼片段如下:
//輸入用戶名 public void enterUsername(String username){ WebElement wl = DriverManager.*getInstance*().findElementById(packagename + ":id/login_input_account"); wl.clear(); wl.sendKeys(username); }
enterUsername()方法中,先根據id定位用戶名輸入框,然后清空輸入框,再輸入用戶名,這些操作都是Appium Client中提供的API。至此演示了登錄case的分層調用過程:cases—>business—>ui—>api。按照分層結構,只要業務邏輯不變,case維護任務主要放在ui層,上層無需改動,如此可極大減少case的維護代價。
在doLogin()方法中有一個“判斷登錄是否成功”的操作,斷言操作是自動化測試用例中必不可少的一部分,下面就開始介紹自動化測試用例的書寫規范。
自動化測試用例書寫規范及注意事項
一條case一般需要包括如下幾個要素:
- 數據准備
- 具體操作
- 驗證點
- 清楚操作結果
數據准備指提前准備測試賬號,假數據等;具體操作得按照業務測試同學提供的文本case來轉化;驗證點指自動化操作后,UI前后的變化點,比如登錄后,首頁會出現用戶名、總資產、財富分等控件,這些都是驗證點;清楚操作結果主要是基於case獨立性的考慮,盡可能做到每條case是獨立的,這樣某條case fail了,也不會對其他case造成影響,當然這樣會增加case的粒度,case穩定性會受影響,兩者之間可自行平衡。
下面介紹case開發的注意事項:
- 元素定位
- ID(首選)
- 文本
- 控件類型
- 坐標
- UI操作需要合理的sleep
- case獨立性
- 封裝常用操作
- 注釋及case前寫明操作步驟
元素定位有ID/文本/控件類型/坐標四種方式,推薦使用ID,因為只要某個控件還存在,其ID變化的可能性很小,若其位置發生了變動,case都無需維護。
下面給出查找元素ID的幾種方法:uiautomatorviewer/hierarchyviewer/源碼。uiautomatorviewer和hierarchyviewer工具在Android SDK中,配置好環境變量后,直接在命令行輸入命令即可打開圖形化工具,hierarchyviewer需要手機root,推薦使用uiautomatorviewer。如果有待測應用的源碼,也可以通過源碼來找ID,當然這種方式比較痛苦,但對熟悉業務及個人成長是有好處的,我最開始就是通過源碼來找ID的。
對於一些奇葩的情況,元素ID/文本/控件類型都無法定位時,使用坐標定位絕對好使,但不到萬不得已,盡量不用,因為用坐標定位就失去了兼容性,換個手機,case可能會執行失敗。
UI操作需要合理的sleep,經常由於網絡原因,UI加載需要一定時間,自動化操作過程中需要合理的sleep,sleep時間短了,case會失敗,長了又浪費時間。Appium框架中引入了隱式sleep方案,在初始化代碼中添加driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);即可,表示每步操作最多等待10s。
case獨立性前面已介紹,封裝及注釋容易理解,不做過多解釋。
功能自動化接入持續集成方案
功能自動化一般用於項目集中測試、回歸測試、dailybuild等,我們不可能通過IDE手動來運行case,一般可借助於jenkins或平台化的方式來批量執行case。下面介紹如何將功能自動化接入jenkins。

接入jenkins主要用到了其定時和輪詢的功能,我們只要准備好構建jar(build.sh)和執行case(execCase.sh)的腳本,放入jenkins的Excute shell模塊,然后配置定時或輪詢的時間即可。build.sh和execCase.sh可參照第四部分介紹的auto_run.sh。具體執行過程如上圖所示:jenkins觸發自動化job;拉取構建站最新apk,拷貝至appium module的apps目錄下;構建測試工程,生成appium.jar(build.sh);批量執行case(execCase.sh),最后jenkins會輸出自動化的執行結果,但是輸出結果可視化程度不好,可自行開發生成報告腳本。至此詳細介紹了基於Appium的功能自動化開發全過程。