1. 概述
UI測試(功能測試、黑盒測試)不需要測試者了解應用程序的內部實現細節,只需要知道當執行了某些特定的動作后是否會得到其預期的輸出。這種測試方法,在團隊合作中可以更好地分離的開發和測試角色。
常見的UI測試的方法是手動去執行,然后去驗證程序是否達到的預期的效果,很顯然這種方法耗時、繁瑣並且很容易出錯。因此我們需要一種可靠的方法來進行UI測試,通過測試框架,我們可以完成針對具體使用場景的測試用例,然后可以循環的、自動的來運行我們的測試case。
Android的SDk提供了以下的工具來支持我們進行UI自動化測試:
uiautomatorviewer:一個用來掃描和分析android應用程序的UI控件的GUI工具。
uiautomator:一個包含創建測試、執行自動化測試API的Java庫。(Uiautomator文檔:http://android.toolib.NET/tools/help/uiautomator/index.html )
要使用這些工具,你必須安裝Android開發工具以下版本:
Android SDKTools:API 21 版本或者21以上版本;
Android SDKPlatform:API 16 版本或者16以上版本.
2. UiAutomatorViewer使用
在你開始寫測試用例之前,使用uiautomatorviewer可以幫助你熟悉你的UI組件(包括視圖和控件)。你可以使用它對當前連接到你電腦上的手機屏幕進行一個快照,然后可以看到手機當前頁面的層級關系和每個控件的屬性。利用這些信息,你可以寫出針對特定UI控件的測試用例。
在 ..\sdk\tools\ 目錄下打開uiautomatorviewer.bat (打開前請手機連接電腦)
1) 獲取快照
當你要分析一個頁面時,首先將手機的頁面停留在你要分析的頁面,然后用數據線連接電腦。然后點擊uiautomatorviewer左上角的第二個圖標按鈕 Device Screenshot,點擊之后會將當前手機界面的快照更新到這里來。
2) 頁面層級
右上方的整個區域,就是當前頁面布局的層級關系。
3) 控件屬性
右下方的整個區域,是當前選中的頁面或者是控件的屬性信息。這部分比較重要,我們以后寫代碼的時候就是需要通過查看屬性中的控件的id或者是text等來獲取控件的實例,然后點擊操作它。
我們可以通過text、resource-id、class、content-desc等來獲取控件。
3. UiAutomator
UiAutomator2.0做了一些改進:
1) 基於 Instrumentation,可以獲取應用 Context,使用 Android 服務及接口
2) 基於 Junit 4,測試用例無需繼承於任何父類,方法名不限,使用注解 Annotation 進行
UI 執行效率比 1.0 快,測試執行可使用Android Junit 方式及 gradle 方式
3) API 更新,新增UiObject2、Until、By、BySelector 等:API For UI Automator
4) Log 輸出變更,以往使用System.out.print 輸出流回顯至執行端,2.0 輸出至 Logcat。
4. UiAutomator2.0使用步驟
1) 在android studio新建一個工程
2) 添加依賴
- androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
- exclude group:'com.android.support',module:'support-annotations'
- })
- testCompile 'junit:junit:4.12'
- // Set this dependencyto build and run UI Automator tests
- androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
3) 在androidTest目錄下新建一個Test, 點擊button4,跳轉到一個新頁面
@RunWith(AndroidJUnit4.class) @SdkSuppress(minSdkVersion = 18) public class Test1 { private UiDevice mDevice; @Before public void before() { // Initialize UiDevice instance mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); assertThat(mDevice, notNullValue()); // Start from the home screen mDevice.pressHome(); // open app openApp("com.ut.anquanguankong"); } @Test public void test() throws InterruptedException { //點擊desc=button4的按鈕 findObject(By.desc("button4")).click(); } public void openApp(String packageName) { Context context = InstrumentationRegistry.getInstrumentation().getContext(); Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivity(intent); } public UiObject2 findObject(BySelector selector) throws InterruptedException { UiObject2 object = null; int timeout = 30000; int delay = 1000; long time = System.currentTimeMillis(); while (object == null) { object = mDevice.findObject(selector); sleep(delay); if (System.currentTimeMillis() - timeout > time) { break; } } return object; } }
5. UiAutomator2.0 API
UiAutomator2.0是兼用1.0的,2.0的API會包含1.0的API。通過了解這API方法,就可以編寫UI自動化測試代碼了。官方文檔:
https://developer.android.google.cn/reference/android/support/test/uiautomator/package-summary.html,下面介紹常用的2.0 API:
1) InstrumentationRegistry
a) 類說明
一個暴露的注冊實例,持有instrumentation運行的進程和參數,還提供了一種簡便的方法調用instrumentation, application context和instrumentation參數。
b) 相關API
返回類型 |
API |
static Bundle |
getArguments(): 返回一個instrumentation參數副本 |
static Context |
getContext(): 返回instrumentation對應包的Context InstrumentationRegistry.getContext() == instrumentation.getContext() |
static Instrumentation |
getInstrumentation(): 返回當前運行的instrumentation |
static Context |
getTargetContext(): 返回一個目標應用程序的Context |
static void |
registerInstance(Instrumentation instrumentation, Bundle arguments):記錄或暴露當前instrumentation運行和instrumentation參數包的副本,存儲在注冊中心 |
c) 示例
@Test public void InstrumentationRegistryTest() { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Context context1 = InstrumentationRegistry.getContext(); Context context2 = InstrumentationRegistry.getTargetContext(); Context context3= instrumentation.getContext(); if(context1 == context2) { Log.i("Chris", "InstrumentationRegistry getContext == getTargetContext"); }else { Log.i("Chris", "InstrumentationRegistry getContext != getTargetContext"); } if(context1 == context3) { Log.i("Chris", "InstrumentationRegistry getContext == Instrumentation getContext"); }else { Log.i("Chris", "InstrumentationRegistry getContext != Instrumentation getContext"); } Intent intent = context2.getPackageManager().getLaunchIntentForPackage("xxx.xxx.xxx"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); context2.startActivity(intent); }
2) UiDevice
a) 類說明
UiDevice用與訪問關設備狀態的信息,也可以使用這個類來模擬用戶在設備上的操作。可以通過下面的方法得到實例:
UiDevice mdevice = getUiDevice();
b) 相關API
返回類型 |
API |
Boolean |
click(int x, int y): 模擬用戶在指定位置點擊 |
String |
getCurrentActivityName(): 獲得的是應用程序在桌面上顯示的名字 |
String |
getCurrentPackageName():獲得當前顯示的應用程序的包名 |
int |
getDisplayHeight():獲得當前設備的屏幕分辨率的高 |
int |
getDisplayWighth():獲得當前設備的屏幕分辨率的寬 |
boolean |
isScreenOn():判斷手機當前是否滅屏 |
Void |
wakeUp():點亮當前屏幕 |
Boolean |
pressBack():點擊back鍵 |
Boolean |
pressHome():點擊home鍵 |
Boolean |
PressMenu():點擊menu鍵 |
Boolean |
PressCode(int code): 利用keycode值模擬一次按下事件, 例如,需要按下數字1 數字1的keycode是 KEYCODE_NUMPAD_1,更多keycode可以在 http://developer.android.com/intl/zh-cn/reference/android/view/KeyEvent.html 進行查詢 |
boolean |
swipe(int startX, int startY, int endX, int endY, int steps): 用指定的步長,從A點滑動B點 |
boolean |
takeScreenshot(File storePath): 截取當前屏幕,保存到文件 |
UiAutomator2在UiDevice新增的API
返回類型 |
API |
void |
dumpWindowHierarchy(OutPutStream out): 獲取當前頁面層級到輸出流 |
String |
executeShellCommand(String cmd): 執行一個shell命令。備注:此方法只支持api21以上,手機需要5.0系統以上 |
UiObject2 |
findObject(BySelector selector): 返回第一個匹配條件的對象 |
UiObject |
findObject(UiSelector selector): 返回一個匹配條件的代表視圖的UiObject對象 |
List<UiObject2> |
findObjects(BySelector selector): 返回所有匹配條件的對象 |
<R> R |
wait(SearchCondition<R> condition, long timeout): 等待的條件得到滿足 |
3) BySelector和By
a) 類說明
BySelector和By是UiAutomator2.0的類。
BySelector類為指定搜索條件進行匹配UI元素,通UiDevice.findObject(BySelector)方式進行使用。
By類是一個實用程序類,可以以簡潔的方式創建BySelectors對象。主要功能是使用縮短語法,提供靜態工廠方法來構造BySelectors對象。例如:你將使用findObject(By.text("foo")),而不是findObject(newSelector().text("foo"))的方式來查找文本值為“foo”的UI元素。
b) 相關API
在這里介紹By的API,BySelector的API和By的對應的。
返回類型 |
API |
BySelector |
clazz(String calssName), clazz(String packageName, String className), clazz(Class clazz), clazz(Pattern className) 通過class來匹配UI |
BySelector |
desc(String contentDescription) descContains(String substring) descStartsWith(String substring) descEndsWith(String substring) desc(Pattern contentDescription) 通過contentDescription來匹配UI |
BySelector |
text(String contentDescription) textContains(String substring) textStartsWith(String substring) textEndsWith(String substring) text(Pattern contentDescription) 通過text來匹配UI |
BySelector |
res(String resourceName) res(String resourcePackage, String resourceId) res(Pattern resourceName) 通過id來匹配UI |
BySelector |
checkable(boolean isCheckable) |
BySelector |
clickable(boolean isClickable) |
BySelector |
enabled(boolean isEnabled) |
BySelector |
focusable(boolean isFocusable) |
BySelector |
focused(boolean isFocused) |
BySelector |
longClickable(boolean isLongClickable) |
BySelector |
scrollable(boolean isScrollable) |
4) UiObject2
1) 類說明
可以理解為直接操作界面ui元素的實例。UiObject2是UiAutomator2的類。
2) 相關API
基本動作
API |
說明 |
clear() |
清楚編輯框內的內容 |
click() |
點擊一個對象 |
clickAndWait(EventCondition<R> condition, long timeout) |
點擊一個對象然后等待在超時的時間內條件滿足則通過,否則拋出異常 |
drag(Point dest, int speed) |
自定義速度拖拽這個對象到指定位置 |
drag(Point dest) |
拖拽這個對象到指定位置 |
longClick() |
長按某個對象 |
scroll(Direction direction, float percent) |
對該對象執行一個滾動操作 |
scroll(Direction direction, float percent, int speed) |
自定義速度,對該對象執行一個滾動操作 |
setText(String text) |
設置文本內容 |
legacySetText(String text) |
通過發送keycode,設置文本內容 |
手勢動作
API |
說明 |
pinchClose(float percent, int speed) |
自定義速度執行收縮手勢 |
pinchClose(float percent) |
執行收縮手勢 |
pinchOpen(float percent, int speed) |
自定義速度執行展開手勢 |
pinchOpen(float percent) |
執行展開手勢 |
fling(Direction direction) |
執行一個掃動手勢,Direction代表為起點方向 |
fling(Direction direction, int speed) |
自定義速度,執行一個掃動手勢 |
swipe(Direction direction, float percent, int speed) |
執行一個滑動操作,可自定義滑動距離和速度 |
swipe(Direction direction, float percent) |
執行一個滑動操作 |
setGestureMargin(int margin) |
以像素為單位,設置手勢邊緣 |
setGestureMargins(int left, int top, int right, int bottom) |
以像素為單位,設置手勢邊緣 |
獲取層級與條件判斷
API |
說明 |
findObject(BySelector selector) |
搜索在這個對象之下的所有元素,並返回第一個與搜索條件匹配的 |
findObjects(BySelector selector) |
搜索在這個對象之下的所有元素,並返回所有與搜索條件匹配的 |
getChildCount() |
返回這個對象直屬子元素的數量 |
getChildren() |
返回這個對象下的直接子元素的集合 |
getParent() |
返回該對象的父類 |
equals(Object object) |
比較兩個對象是否相等 |
hashCode() |
獲取對象的哈希碼 |
hasObject(BySelector selector) |
返回該對象是否存在 |
recycle() |
回收該對象 |
wait(UiObject2Condition<R> condition, long timeout) |
等待條件被滿足 |
wait(SearchCondition<R> condition, long timeout) |
等待條件被滿足 |
5) Configration
a) 類說明
Configrator用於設置腳本動作的默認延時:
1. 可調節兩個模擬動作之間的默認間隔
2. 可調節輸入文本的輸入時間間隔
3. 可調節每次滾動的時間間隔
4. 可調節等待系統空閑的默認時間
b) 相關API
延時項 |
默認延時 |
說明 |
API |
動作 |
3s |
設置延時 |
setActionAcknowledgmentTimeout(long timeout) |
|
|
獲取默認延時 |
getActionAcknowledgmentTimeout() |
鍵盤輸入 |
0s |
設置延時 |
setKeyInjectionDelay(long delay) |
|
|
獲取默認延時 |
getKeyInjectionDelay() |
滾動 |
200ms |
設置延時 |
setScrollAcknowledgmentTimeout(long timeout) |
|
|
獲取默認延時 |
getScrollAcknowledgmentTimeout() |
空閑 |
10s |
設置延時 |
setWaitForIdleTimeout(long timeout) |
|
|
獲取默認延時 |
getWaitForIdleTimeout() |
組件查找 |
10s |
設置延時 |
setWaitForSelectorTimeout(long timeout) |
|
|
獲取默認延時 |
getWaitForSelectorTimeout() |
6. 斷言
1) 斷言函數介紹
確定被測試的方法是否按照預期的效果正常工作
比如說:
if (假設成立){
通過測試
}else{
報錯並終止當前用例測試
}
2) 斷言函數用例結構
一個完整的測試用例必需要有斷言函數
setUp//初始化
//測試用例,junit4版本才可以使用多條用例
test 初始化場景與數據
test 模擬操作步驟
test 斷言
test 恢復場景
tearDown//回收初始化垃圾
3) 斷言函數Java錯誤類型
a) Error:
一般是指與虛擬機相關的問題,如系統崩潰,虛擬機錯誤,內存空間不足,方法調用棧溢出等。對於這類錯誤導致的應用程序中斷,僅靠程序本身無法恢復和預防(斷言)
b) Exeeption:
表示程序可以處理的異常,可以捕獲且可能恢復。遇到這類異常,應該盡可能處理異常,使程序恢復運行,而不應該隨意終止異常(最常見的是UI對象找不到的異常)
4) 斷言函數API

例如:
//斷言兩個對象是否相等
asserEquals(Stringmessage,Object expected,Object actual){
if (expected==null && actual==null){
return ;
}
if (expected!=null && expected.equals(actual)){
return
}
failNotEquals(message,expected,actual);
}
參數 |
說明 |
Message |
可選消息,在斷言失敗后會拋出這個消息 |
Expected |
期望的值 |
Actual |
實際的值 |
相關API
方法 |
說明 |
assertEquals(boolean,boolean) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,boolean,boolean) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(byte,byte) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,byte,byte) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(char,char) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,char,char) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(int,int) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,int,int) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(long,long) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,long,long) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(Object,Object) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,Object,Object) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(short,short) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,short,short) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,String) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(String,String,String) |
如果期望(expected)和實際(actual)相等則通過,否則失敗 |
assertEquals(double,double,double) |
如果期望(expected)和實際(actual)相差不超過精度值(delta)則通過,否則失敗 |
assertEquals(String,double, double,double) |
如果期望(expected)和實際(actual)相差不超過精度值(delta)則通過,否則失敗 |
assertEquals(float,float,float) |
如果期望(expected)和實際(actual)相差不超過精度值(delta)則通過,否則失敗 |
assertEquals(String,float,float,float) |
如果期望(expected)和實際(actual)相差不超過精度值(delta)則通過,否則失敗 |
assertFalse(boolean) |
如果條件(condition)為False則通過,否則失敗 |
assertFalse(String,boolean) |
如果條件(condition)為False則通過,否則失敗 |
assertTrue(boolran) |
如果條件(condition)為True則通過,否則失敗 |
assertTrue(String,boolran) |
如果條件(condition)為True則通過,否則失敗 |
assertNotNull(Object) |
如果條件(condition)為非空則通過,否則失敗 |
assertNotNull(String,Object) |
如果條件(condition)為非空則通過,否則失敗 |
assertNull(Object) |
如果條件(condition)為空則通過,否則失敗 |
assertNull(String,Object) |
如果條件(condition)為空則通過,否則失敗 |
assertNotSame(Object,object) |
如果期望(expected)和實際(actual)引用不同的內存對象對象則通過,否則失敗 |
assertNoteSame(String,Object,Object) |
如果期望(expected)和實際(actual)引用不同的內存對象對象則通過,否則失敗 |
assertSame(Object,Object) |
如果期望(expected)和實際(actual)引用相同的內存對象對象則通過,否則失敗 |
assertSame(String,Object,Object) |
如果期望(expected)和實際(actual)引用相同的內存對象對象則通過,否則失敗 |
fail() |
用例立即失敗 |
fail(String) |
用例立即失敗,且拋出指定消息 |
failNotEquals(String,Object,Object) |
用例立即失敗,且拋出指定消息與期望、實際值不相等的消息 |
failNotSame(String,String,String) |
用例立即失敗,且拋出指定消息與期望、實際值不相等的消息 |
failSame(String) |
用例立即失敗,且拋出指定消息 |
8. 報告分析
1) 錯誤類型
斷言錯誤:就是斷言這個用例的成功或者失敗(AssrtionFailedError)
腳本錯誤:UiObjectNotFoundException(找不到對象異常)、java異常等
2) 報告分析
@Test public void testMain() throws InterruptedException, UiObjectNotFoundException { BySelector tabSelector = By.desc("TabContainer"); uiAction.click(By.desc("button4")).isExist("打開主頁面出錯", tabSelector); mDevice.pressBack(); }
這個方法測試:點擊button4,進入主頁面。
a) 正常運行
testMain打開一個存在的頁面。
run started: 1 tests TestRunner: started: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) MonitoringInstrumentation: Activities that are still in CREATED to STOPPED: 0 InteractionController: runAndwaitForEvents timed out waiting for events QueryController: Got null root node from accessibility - Retrying... InteractionController: runAndwaitForEvents timed out waiting for events TestRunner: finished: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) MonitoringInstrumentation: Activities that are still in CREATED to STOPPED: 0 TestRunner: run finished: 1 tests, 0 failed, 0 ignored
從上面報告來看,testMain正常執行。
b) 斷言錯誤
testMain打開一個不存在的頁面。
run started: 1 tests TestRunner: started: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) MonitoringInstrumentation: Activities that are still in CREATED to STOPPED: 0 InteractionController: runAndwaitForEvents timed out waiting for events QueryController: Got null root node from accessibility - Retrying... TestRunner: failed: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) TestRunner: ----- begin exception ----- TestRunner: junit.framework.AssertionFailedError: 打開主頁面出錯 at junit.framework.Assert.fail(Assert.java:50) at junit.framework.Assert.assertTrue(Assert.java:20) at junit.framework.Assert.assertNotNull(Assert.java:218) at com.chris.example.uiautomatordemo.UiAutomatorActionImpl.isExist(UiAutomatorActionImpl.java:102) at com.chris.example.uiautomatordemo.AnquanguankongTest.testMain(AnquanguankongTest.java:52) TestRunner: ----- end exception ----- TestRunner: finished: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) MonitoringInstrumentation: Activities that are still in CREATED to STOPPED: 0 TestRunner: run finished: 1 tests, 1 failed, 0 ignored
從上面報告來看,testMain執行失敗,並給出詳細的錯誤信息。
c) 腳本錯誤
testMain點擊一個不存在的button
run started: 1 tests TestRunner: started: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) MonitoringInstrumentation: Activities that are still in CREATED to STOPPED: 0 InteractionController: runAndwaitForEvents timed out waiting for events QueryController: Got null root node from accessibility - Retrying... TestRunner: failed: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) TestRunner: ----- begin exception ----- TestRunner: junit.framework.AssertionFailedError: BySelector [DESC='\Qbutton42\E'] no found
at junit.framework.Assert.fail(Assert.java:50)
at junit.framework.Assert.assertTrue(Assert.java:20)
at junit.framework.Assert.assertNotNull(Assert.java:218)
at com.chris.example.uiautomatordemo.UiAutomatorActionImpl.isExist(UiAutomatorActionImpl.java:112)
at com.chris.example.uiautomatordemo.UiAutomatorActionImpl.findObjectWithCheck(UiAutomatorActionImpl.java:75)
at com.chris.example.uiautomatordemo.UiAutomatorActionImpl.click(UiAutomatorActionImpl.java:84)
at com.chris.example.uiautomatordemo.AnquanguankongTest.testMain(AnquanguankongTest.java:52)
TestRunner: ----- end exception ----- TestRunner: finished: testMain(com.chris.example.uiautomatordemo.AnquanguankongTest) MonitoringInstrumentation: Activities that are still in CREATED to STOPPED: 0 TestRunner: run finished: 1 tests, 1 failed, 0 ignored