基於 Appium 的 Android UI 自動化測試
自動化測試是研發人員進行質量保障的重要一環,良好的自動化測試機制能夠讓開發者及早發現編碼中的邏輯缺陷,將風險前置。日常研發中,由於快速迭代的原因,我們經常需要在各個業務線上進行主流程回歸測試,目前這種測試大部分由人工進行,費時費力,重複勞動多。如果能將UI自動化測試與主流程回歸結合到一起,一方面保證了代碼質量,另一方面大大節約人力成本,可謂一舉兩得。
為什麼需要UI自動化測試
原因主要是以下三點:
- 保證質量——及早發現代碼缺陷,風險前置。
- 減少重複勞動,節約人力——快速迭代中經常需要進行主流程回歸,測試完整個主流程,需要耗費相當大的人力成本。
- 統一標準——每個人對測試用例以及業務理解程度不同,標準可能存在不一致。
進行UI自動化測試面臨的問題
- 工具選擇。
- 降低對後端的依賴,避免因為測試環境後端不穩定導致的測試失敗。
- 整合測試用例,增加復用,降低用例維護成本。
自動化測試工具對比
業界UI測試工具發展迅速,目前有Robotium、Appium、Espresso、UIAutomator、Calabash等等,其中在Android中應用最廣泛的當屬UIAutomator、Robotium、Appium。
下面列表比較說明:
UIAutomatorRobotiumAppium支持平台AndroidAndroid,H5Android,iOS,H5腳本語言JavaJavaAlmost any是否支持無源碼測試YesYesYes支持API級別16+AllAll除了Android、Hybrid類型的App,Appium還可以在iOS設備上運行。加上之前組內有同事做過Appium方面的分享,在這方面有一定的基礎,所以最終我們選擇了Appium。
介面穩定性與數據可變性
業務特性決定我們的case在運行過程中會經常向後端請求數據,然後根據後端介面返回的數據決定頁面元素展示。因此,有兩個難點是必須克服的:
- 後端介面穩定性
測試環境並不像線上,能在7x24內保持穩定。業務介面經常出現因所依賴的外部環境異常而請求失敗的情況,以往處理這種情形,我們能做的事情往往很有限,最糟糕的就是必須要等待第三方修改完成後,才能繼續我們的測試。因此,如何保持介面穩定,將成為UI自動化測試不得不面對的問題。
- 測試數據配置與保存克服了1中提到的介面穩定難點後,仍然要面對第二個難點——頻繁修改配置以適應測試用例的條件。舉個例子,對於閃惠業務,用例裡面會對於商戶配置的多種情況進行測試(無優惠、有優惠未開始、僅有閃惠優惠、有閃惠和團購、閃惠打折、閃惠贈品等),這裡面的條件是複雜多變的。如果每一次進行測試前,都由執行測試人在商戶後台登錄後手動修改配置,將耗費巨大的人力成本。因此我們勢必找出一條途徑,將這種繁瑣的配置過程自動化。
接入Appmock
註:使用Appmock,需建立在App底層網路請求模塊已經具備切換mock地址的功能的基礎上。
Appmock是美團點評平台組製作的非常優秀的mock工具,其前身是美團點評同事張文東所編寫的wendong.dp(僅供美團點評內部使用)。在Appmock上可以進行網路請求的查看與mock。那麼,是否可以讓我們的自動化測試用例在運行時訪問Appmock,獲取預設的mock數據呢?做過相關App開發的同事都知道,在App中這是很容易實現的,只要訪問某個特定HTTP鏈接進行註冊即可。
Appmock使用界面由此,「後端介面穩定性」的問題,在Appmock的幫助下就解決了,如果把後端數據直接配置在Appmock上,請求失敗的概率就微乎其微。即便如此,仍然要面臨頻繁修改配置的需求,只不過是把修改的操作從商家後台頁面轉移到了mock系統。有沒有什麼方法,可以讓修改配置的操作自動化進行呢?
在研讀過Appmock的源碼後,我們想到,可以自己搭建一個mock-server,把不同階段的mock數據保存在資料庫中,並且開放出網路介面,用來切換各個測試用例所需的mock數據。具體的系統結構如下圖所示。
上圖描述了一次用例運行的簡要過程,事前需要在資料庫中準備好測試數據,mock-server基於Appmock,使用NodeJS進行二次開發完成。
編寫測試用例
為了簡化用例編寫,減少開發與維護的工作量,使用Page Object模式進行用例開發。
Page Object定義為抽象頁面的對象,通過對頁面功能的封裝,進行相應操作。它的優點是:
- 減少重複代碼,增加復用性。
- 提高代碼可讀性、穩定性。
- 易於維護。
UI自動化測試框架的編寫方式類似於MVC架構,我們將測試用例中的業務邏輯、各個頁面間的元素以及測試數據相分離後獨立編寫,以下均用排隊業務的主流程舉例。
測試類組成
測試類的組成包括setUp(),tearDown()方法以及各個測試用例testXXXX(),所有的測試用例必須以小寫test開頭,如正常排號下的testQueueNormalQueue():
@Beforenpublic void setUp() throws Exception {n File apk = new File(APK_NOVA);n DesiredCapabilities capabilities = DesiredCapabilities.android();n capabilities.setCapability("device", Platform.ANDROID);n capabilities.setCapability(CapabilityType.VERSION, "5.1");n …… // capabilities各個常量欄位n driver = new AndroidDriver<AndroidElement>(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);n splashScreen = new SplashScreen(driver);n mainPage = new MainPage(driver);n …… // Page Object初始化n}n@Afternpublic void tearDown() throws Exception {n driver.quit();n}n@Testnpublic void testQueueNormalQueue() {n // 略n}n
測試用例中不用直接對頁面元素進行操作,我們所要做的事情僅僅是業務層面的邏輯,包括表單數據的提交、頁面按鈕的點擊跳轉等等。
頁面類編寫
頁面類的編寫採用Page Object模式,包括頁面中會使用到的元素、頁面元素的操作方法集以及頁面元素的檢驗方法集。
所有的Page子類均繼承BasePage父類,它要做的事情很簡單,無非就是1個driver,2個driverWait用於延時載入的等待時間,以及頁面元素的初始化:public class BasePage {n private static final int TIMEOUT = 1; // short timeout for web-elementn private static final int TIMEOUT_LONG = 10; // long timeout for web-elementn public AndroidDriver<AndroidElement> driver;n public WebDriverWait driverWait;n public WebDriverWait driverLongWait;n public BasePage(AndroidDriver<AndroidElement> driver) {n this.driver = driver;n this.driverWait = new WebDriverWait(this.driver, TIMEOUT);n this.driverLongWait = new WebDriverWait(this.driver, TIMEOUT_LONG);n PageFactory.initElements(this.driver, this); // 這句非常重要,如果不寫的話儘管編譯不會報錯,但是後面要說的頁面元素在運行時一個都找不到n }n}n
然後是各個Page子類的實現方法:
public class ShopInfoPage extends BasePage {n public ShopInfoPage(AndroidDriver<AndroidElement> driver) {n super(driver);n }n …… // 頁面元素 @FindByn …… // 操作方法,比如login()、clickXXXXXXButton()、gotoXXXXXXPage()n …… // 檢驗方法,比如checkLoaded()、checkLoginSuccess()、checkQueue_LoginReadyQueue()n}n
Page子類的元素定位我們使用@FindBy註解方式進行統一的管理。
元素定位最基本的方法就是使用id/name/class等,如果不行的話就用相對複雜卻無所不能的xpath,如:
// 點擊登錄按鈕n@FindBy(id = "login_tip")nprivate WebElement clickLoginButton;nnn// MAPI域名輸入框n@FindBy(xpath = "//*[contains(@resource-id, id/mapi_item)]//*[contains(@resource-id, id/debug_domain)]")nprivate WebElement mapiDomainText;n
Page中的操作和檢驗方法調用已經封裝好的BaseUtils中的方法,如:
BaseUtils.waitForElement(driverWait, loginButton).click(); // 等待元素出現並點擊nAssert.assertTrue(BaseUtils.waitForElementVisibility(driverLongWait, usernameText)); // 檢驗元素應該展示在頁面上n
BaseUtils方法
BaseUtils中封裝好了一些通用的方法,還需要不斷完善並擴展。下面介紹其中一些常用及重要的方法:
- openDebugPanel():每次直接調用該方法來打開Debug面板,由於Debug面板是一個系統層面的懸浮窗,它不屬於任何頁面中的元素(你完全沒辦法通過ID甚至XPath獲得)。
- clickPoint():點擊某個坐標+持續時間,坐標採用相對屏幕位移的方式(左上為0,0),這裡只實現了簡單的單指的點擊操作,實際上driver.tap可以模擬多指的共同操作。
- swipeToUp() & swipeToDown():上拉 & 下拉頁面操作,需要傳的是次數和每次持續時間,模擬手指在屏幕上的滑屏操作,主要用於刷新頁面以及繞過某些有坑的scrollTo。
- prepareMockData():這裡要做的就是,在關鍵步驟操作前傳入mock_data_id,我們會將數據請求發送給伺服器,然後伺服器從資料庫拉到對應的mock data並更新。
- saveScreenshot():顧名思義,截圖。在每個重要的頁面操作方法中加入即可,需要傳入的是case_id以及操作或檢查時的keyword,方便在用例執行完以後看截圖分析和Bug復現。
- waitForElementXXX():在預設等待時間內等待元素出現並定位元素。
UI自動化測試運行效果
在排隊與閃惠兩條業務線進行了UI自動化測試實踐,它們執行完成全套用例的耗時均不超過20min。相比於之前人工進行主流程測試動輒花費半天的工作量的情況,大大降低了人力成本,將工程師寶貴的時間節約給了更有價值的研發工作。
當然,自動化測試前期的環境搭建、數據準備、用例編寫等任務是必不可少的,這些準備工作很多都是一次性投入,一勞永逸,也正是自動化測試的價值所在。
參考資料
- Appium Doc
- Page Object Design Pattern
不想錯過技術博客更新?想給文章評論、和作者互動?第一時間獲取技術沙龍信息?
請關注我們的官方微信公眾號「美團點評技術團隊」。
推薦閱讀:
※給力!據說裝了這些APP,就能讓你的安卓機徹底和卡頓說再見
※SRC 對於音質會有怎樣的影響?
※「合理」的修改簡訊時間,有時候是很必要的
※Android Things Developer Preview 4.1來了
※關於Android中App的停止狀態