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以上版本.
在你开始写测试用例之前,使用uiautomatorviewer可以帮助你熟悉你的UI组件(包括视图和控件)。你可以使用它对当前连接到你电脑上的手机屏幕进行一个快照,然后可以看到手机当前页面的层级关系和每个控件的属性。利用这些信息,你可以写出针对特定UI控件的测试用例。
在 ..\sdk\tools\ 目录下打开uiautomatorviewer.bat (打开前请手机连接电脑)
1) 获取快照
当你要分析一个页面时,首先将手机的页面停留在你要分析的页面,然后用数据线连接电脑。然后点击uiautomatorviewer左上角的第二个图标按钮 Device Screenshot,点击之后会将当前手机界面的快照更新到这里来。
2) 页面层级
右上方的整个区域,就是当前页面布局的层级关系。
3) 控件属性
右下方的整个区域,是当前选中的页面或者是控件的属性信息。这部分比较重要,我们以后写代码的时候就是需要通过查看属性中的控件的id或者是text等来获取控件的实例,然后点击操作它。
我们可以通过text、resource-id、class、content-desc等来获取控件。
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。
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; } }
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:
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); }
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 |
findObjects(BySelector selector): 返回所有匹配条件的对象 |
|
wait(SearchCondition |
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) |
1) 类说明
可以理解为直接操作界面ui元素的实例。UiObject2是UiAutomator2的类。
2) 相关API
基本动作
API |
说明 |
clear() |
清楚编辑框内的内容 |
click() |
点击一个对象 |
clickAndWait(EventCondition |
点击一个对象然后等待在超时的时间内条件满足则通过,否则抛出异常 |
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 |
等待条件被满足 |
wait(SearchCondition |
等待条件被满足 |
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() |
确定被测试的方法是否按照预期的效果正常工作
比如说:
if (假设成立){
通过测试
}else{
报错并终止当前用例测试
}
一个完整的测试用例必需要有断言函数
setUp//初始化
//测试用例,junit4版本才可以使用多条用例
test 初始化场景与数据
test 模拟操作步骤
test 断言
test 恢复场景
tearDown//回收初始化垃圾
a) Error:
一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误导致的应用程序中断,仅靠程序本身无法恢复和预防(断言)
b) Exeeption:
表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常(最常见的是UI对象找不到的异常)
例如:
//断言两个对象是否相等
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) |
用例立即失败,且抛出指定消息 |
断言错误:就是断言这个用例的成功或者失败(AssrtionFailedError)
脚本错误:UiObjectNotFoundException(找不到对象异常)、java异常等
@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
从上面报告来看,testMain执行失败,并给出详细的错误信息。