爲了不每次上線前重複的人工迴歸測試,保證每次上線的版本不會引發核心業務的不穩定,因此急需自動化測試來保證業務的穩定性.通過調研我嘗試使用Appium進行自動化測試,緣由是功能強大,跨平臺並且社區也很活躍.html
若是有不清楚WebDriver的小夥伴立刻在Appium架構介紹中會有說明.java
iOS: 蘋果的UIAutomation
Android 4.2+: Google的UiAutomator
Android 2.3+: Google's Instrumentation. (由單獨的項目Selendroid提供支持 )android
Appium 1.6版本以上增長了UiAutomator2
git
爲了知足上面跨平臺,把這些三方框架封裝成一套API —— WebDriver Api(客戶端到服務端的協議)github
事實上 WebDriver 已經成爲 web 瀏覽器自動化的標準,也成了 W3C 的標準 —— W3C Working Draft,因此Appium在原有基礎上擴充了移動自動化相關的API.web
投資 WebDriver 意味着你能夠押寶在一個已經成爲標準的獨立,自由和開放的協議。你不會被任何專利限制。npm
核心架構: Appium使用C/S架構,運行時候Service端會監聽Client端發送的命令,接着在移動設備上執行這些命令,而後將執行結果放在 HTTP 響應中返還給客戶端.瀏覽器
基於這架構能夠作什麼?bash
下圖解釋了雲服務的具體做用:服務器
安裝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語法不夠簡潔或者不熟悉,可使用你所熟悉的語言.
建立工程,並加入下面依賴:
<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 客戶端的依賴就引入成功了.
每一個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的一些註釋,作準備化和收尾操做.
上面的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等信息):
用 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 只須要簡單的控制頁面元素便可.
而後在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文檔不全並且有點亂
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);