概述
為了避免每次上線前重複的人工迴歸測試,保證每次上線的版本不會引起核心業務的不穩定,所以急需自動化測試來保證業務的穩定性.經過調研我嘗試使用Appium進行自動化測試,原因是功能強大,跨平臺而且社群也很活躍.
主流框架對比
Appium優點
- 開源
- 跨架構:Native App、Hybird App、Web App
- 跨裝置:Android、iOS、Firefox OS
- 不依賴原始碼
- 使用任何 WebDriver 相容的語言來編寫測試用例。比如 Java, Objective-C, JavaScript with Node.js (in both callback and yield-based flavours), PHP, Python, Ruby, C#, Clojure, 或者 Perl.
- 不需要重新編譯APP
如果有不清楚WebDriver的小夥伴馬上在Appium架構介紹中會有說明.
Appium理念
- 你無需為了自動化,而重新編譯或者修改你的應用。
- 你不必侷限於某種語言或者框架來寫和執行測試指令碼。
- 一個移動自動化的框架不應該在介面上重複造輪子。(移動自動化的介面應該統一)
- 無論是精神上,還是名義上,都必須開源。
Appium架構
iOS: 蘋果的UIAutomation
Android 4.2+: Google的UiAutomator
Android 2.3+: Google's Instrumentation. (由單獨的專案Selendroid提供支援 )
Appium 1.6版本以上增加了UiAutomator2
為了滿足上面跨平臺,把這些三方框架封裝成一套API —— WebDriver Api(客戶端到服務端的協議)
事實上 WebDriver 已經成為 web 瀏覽器自動化的標準,也成了 W3C 的標準 —— W3C Working Draft,所以Appium在原有基礎上擴充了移動自動化相關的API.
投資 WebDriver 意味著你可以押寶在一個已經成為標準的獨立,自由和開放的協議。你不會被任何專利限制。
核心架構: Appium使用C/S架構,執行時候Service端會監聽Client端傳送的命令,接著在移動裝置上執行這些命令,然後將執行結果放在 HTTP 響應中返還給客戶端.
基於這架構可以做什麼?
- 可以用任何實現了該客戶端的語言來寫測試程式碼
- 可以把服務端放在不同的機器上
- 可以只寫測試程式碼,然後利用類似 saucelabs 雲服務來解釋命令.
下圖解釋了雲服務的具體作用:
Appium 使用
服務端
-
安裝Appium伺服器
npm install -g appium npm install -g appium-doctor appium-doctor 複製程式碼
其中appium-doctor
用來檢查電腦是否缺少相關依賴.當所有都是對勾表示Appium環境配置完畢,如下:
-
開啟appium伺服器:
appium --address 127.0.0.1 --port 4723 --log "/Users/mio4kon/Desktop/ appium.log" --log-timestamp --local-timezone --session-override 複製程式碼
客戶端
再次強調 Appium
支援各種語言,這裡我選擇JAVA.如果覺得JAVA語法不夠簡潔或者不熟悉,可以使用你所熟悉的語言.
建立 MAVEN/Gradle 工程:
建立工程,並加入下面依賴:
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>5.0.0-BETA2</version>
<exclusions>
<exclusion>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.0.1</version>
</dependency>
複製程式碼
這樣Appium 客戶端的依賴就引入成功了.
Capabiltiy配置:
每個Test的基類中定義setUp
方法,並設定Capabiltiy
以前其他的初始化操作:
DesiredCapabilities capabilities = new DesiredCapabilities ();
capabilities.setCapability (MobileCapabilityType.DEVICE_NAME, deviceName);
capabilities.setCapability (MobileCapabilityType.PLATFORM_NAME, platformName);
capabilities.setCapability (MobileCapabilityType.PLATFORM_VERSION, platformVersion);
capabilities.setCapability (MobileCapabilityType.APP, apkPath);
capabilities.setCapability (AndroidMobileCapabilityType.APP_PACKAGE, appPackage);
capabilities.setCapability (AndroidMobileCapabilityType.APP_ACTIVITY, appActivity);
capabilities.setCapability (MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2);
複製程式碼
這裡我使用的是 TestNG 中的Parameters
註釋來配置引數.
TestNG 又是什麼鬼? 簡單來說TestNG是java的一個測試框架,類似JUnit但功能更加強大,使用也方便.
TestNG
利用TestNg的一些註釋,做準備化和收尾操作.
上面的setUp
方法我就使用了BeforeClass
和Parameters
這兩種註釋.
@BeforeClass
@Parameters({"driverName", "url", "deviceName", "platformName", "platformVersion", "apkPath", "appPackage", "appActivity"})
public void setUp(String driverName, String url, String deviceName,
String platformName, String platformVersion, String apkPath,
String appPackage, String appActivity) throws Exception {
log.i (TAG, "BeforeClass");
driver = setRemoteDriver (driverName, url, deviceName, platformName, platformVersion, apkPath, appPackage, appActivity);
actions = ElementActions.getInstance ().init (driver);
assertActions = actions.getAssertActions ();
Screenshot.getInstance ().init (driver);
prepare ();
}
複製程式碼
同樣在在AfterClass
後,進行了driver
的退出.
正常情況下我們寫測試用例是下面這種情況:
一個TestClass
中包含多個TestMethod
.如果每個TestMethod
都相互獨立,需要重新執行APP顯然非常耗時,所以這裡我將每一個TestClass
相互獨立,而其中的TestMethod
又相互依賴,執行順序通過TestNG
的XML來控制.如下:
<test name="XX測試">
<classes>
<class name="XXTest">
<methods>
<include name="testAAA"/>
<include name="testBBB"/>
<include name="testCCC"/>
</methods>
</class>
</classes>
</test>
複製程式碼
但是有些情況如果需要TestMethod
也相互獨立的話,可以利用AfterMethod
註釋.
@AfterMethod
public void afterMethod() {
driver.resetApp ();
}
複製程式碼
編寫用例
查詢元素
定位方式
查詢元素可以通過很多方法(可以通過UIAutomatorViewer
獲取頁面的id,name等資訊):
- id
- name
- className
- xpath
- uiautomator
高階定位
-
用
xpath
查詢登入按鈕:by.xpath ("//button[@name='login']") 複製程式碼
- 用
uiautomator
的API滾動查詢:
String rule = "new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textContains(\"" + locator.value + "\"))"
WebElement cl = driver.findElementByAndroidUIAutomator(rule));
複製程式碼
其實就是用如果用uiautomator來寫:
new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().textContains(value));
複製程式碼
### 元素管理
複製程式碼
為了讓測試用例能夠方便複用,簡化寫用例的時間,必要的封裝是不可少的. 利用 Page Object 模式將頁面上的元素進行封裝.這樣所有 Test 只需要簡單的控制頁面元素即可.
- 可以使用yaml檔案進行管理頁面元素:
然後在BasePage中將yaml解析封裝成Locator
物件.並儲存到集合.
這樣所有的PageObject都可以通過下面方法來定位元素.
protected Locator getLocator(String locatorName) {
checkNotNull (locatorName);
Page page = getPage ();
List<Locator> locators = page.locators;
for(Locator locator : locators) {
if (locatorName.equals (locator.name)) {
return locator;
}
}
return null;
}
public Locator 請輸入手機號 = getLocator ("請輸入手機號");
public Locator 請輸入驗證碼 = getLocator ("請輸入驗證碼");
public Locator 傳送驗證碼 = getLocator ("傳送驗證碼");
複製程式碼
TODO: 用工具生成對應page類
之所這麼做是可以在用的時候直接通過IDE提示有哪些控制元件,但是寫上述程式碼缺都是無聊的操作,所有我用了 freemarker 自動解析yaml來生成對應Page類,如下:
Map<String, Object> input = new HashMap<String, Object> ();
input.put ("pageName", page.pageName);
List<LocatorObject> genLocators = new ArrayList<> ();
List<Locator> locators = page.locators;
for(int i = 0; i < locators.size (); i++) {
System.out.println ("生成Locator:" + locators.get (i).name);
genLocators.add (new LocatorObject (locators.get (i).name, locators.get (i).name));
}
input.put ("locators", genLocators);
Template template = cfg.getTemplate ("page-temp.ftl");
Writer fileWriter = new FileWriter (new File (path, page.pageName + ".java"));
try {
template.process (input, fileWriter);
} finally {
fileWriter.close ();
}
複製程式碼
如果不會用 freemarker
的可以搜尋下相關資料.
元素互動
將互動事件,封裝在ElementActions
中.使用起來非常簡單:
actions.text (loginPage.請輸入手機號, phone);
actions.click (loginPage.傳送驗證碼);
actions.text (loginPage.請輸入驗證碼, pwd);
actions.click (loginPage.登入);
複製程式碼
常用的互動事件:單擊,連擊,上下左右滑動,後退,輸入文字等等.
上面元素定位其實只是封裝Locator,並沒有真正的查詢元素,查詢元素實際上是在ElementActions
中處理的.由於應用經常處理網路請求,控制元件有時候需要過段時間才可見,所以在查詢控制元件時候需要給予一定的時間.
WebElement element;
try {
element = (new WebDriverWait (mDriver, locator.timeOutInSeconds)).until (
new ExpectedCondition<WebElement> () {
@Override
public WebElement apply(WebDriver driver) {
List<WebElement> elements = getElement (locator);
if (elements.size () != 0) {
return elements.get (0);
}
return null;
}
});
} catch (NoSuchElementException | TimeoutException e) {
log.e (TAG, "超時[%4$d秒],找不到元素:[%1$s] , [By.%2$s : %3$s]", locator.name, locator.type, locator.value, locator.timeOutInSeconds);
throw e;
}
複製程式碼
斷言
同樣為了使用方便講常用的斷言封裝,如Toast驗證等.
public void validatesToast(final String msg) {
checkNotNull (msg);
final WebDriverWait wait = new WebDriverWait (mDriver, 10);
assertNotNull (wait.until (ExpectedConditions
.presenceOfElementLocated (By.xpath (String.format ("//*[@text=\'%s\']", msg)))));
}
複製程式碼
測試報告
測試報告可以直觀的展示測試的成功率,截圖等資訊.這裡選擇extentreports做為測試報告的框架.
最終效果:
踩坑
findElementByName
無效.
Searching by name was deprecated over a year ago and removed from 1.5. In general, searching by accessibility id is better for a variety of reasons.
如上findElementByName
這個方法從Appium 1.5
之後刪除了,但是API不經能找到並且也沒提示過時.這不坑爹嘛.後來使用下面的程式碼才解決用name查詢元素的方法.
String query = "new UiSelector().textContains" + "(\"" + locator.value + "\")";
webElements = mDriver.findElementsByAndroidUIAutomator (query);
複製程式碼
- 據說
Appium 1.6.3
可以查詢 Toast 的資訊了.然後屁顛屁顛的跑去試了下網上的例子發現不好使啊.一度以為是Client版本的問題.搞了半天才發現需要加下面的程式碼:
capabilities.setCapability (MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2);
複製程式碼
(感覺現在的Appium文件不全而且有點亂
- 無意中發現測試的時候高德地圖彈Toast報錯.然後直接編譯安裝卻不儲存.猜測是不是在安裝過程中Appium改了啥.看了下Service日誌,竟然在安裝的時候重新簽名...
App not signed with debug cert.
2017-02-13 18:17:19:848 - info: [debug] [ADB] Resigning apk.
2017-02-13 18:17:23:938 - info: [debug] [ADB] Zip-aligning 'app-debug.apk'
2017-02-13 18:17:23:958 - info: [ADB] Checking whether zipalign is present
2017-02-13 18:17:23:964 - info: [ADB] Using zipalign from /Users/mio4kon/Library/Android/sdk/build-tools/25.0.2/zipalign
2017-02-13 18:17:23:968 - info: [debug] [ADB] Zip-aligning apk.
2017-02-13 18:17:24:104 - info: [AndroidDriver] Remote apk path is /data/local/tmp/463eb03788048b4a1dacfe28545ee76e.apk
複製程式碼
解決方法:
capabilities.setCapability (AndroidMobileCapabilityType.NO_SIGN, true);