UiAutomator源码分析之获取控件信息
最后更新于:2022-04-01 19:55:36
根据上一篇文章《[UiAutomator源码分析之注入事件](http://blog.csdn.net/zhubaitian/article/details/40541927)》开始时提到的计划,这一篇文章我们要分析的是第二点:
- 如何获取控件信息
我们在测试脚本中初始化一个UiObject的时候通常是像以下这个样子:
~~~
UiObject appsTab = new UiObject(new UiSelector().text("Apps"));
appsTab.click()
~~~
那么这个过程发生了什么呢?这就是我们接下来要说的事情了。
## 1. 获取控件信息顺序图
这里依然是一个手画的不规范的顺序图,描述了UiObject尝试获得一个控件的过程中与相关的类的交互,这些类的关系在《[UiAutomator源码分析之UiAutomatorBridge框架](http://blog.csdn.net/zhubaitian/article/details/40539103)》中已经进行了描述。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e2a2815b.jpg)
这里整一个过程并不复杂,简单说明下就这几点:
- UiObject对象几经周折通过不同的类最终联系上UiAutomation,然后通知UiAutomation对象它想取得当前活动窗口的所有元素的AccessibilityNodeInfo类型的根节点
- AccessibilityNodeInfo代表了屏幕中控件元素的一个节点,同时它也拥有一些成员方法可以以当前节点为基础来获得其他目标节点。可以把屏幕上的节点想像成是通过类似xml的格式组织起来的,所以一旦知道根节点和由选择子UiSelector指定的目标控件信息,我们就可以遍历整个窗口控件
- QueryController对象获得Root Node之后,就是调用tranlateCompoundSelector这个方法来遍历窗口所有控件,直到找到选择子UiSelector指定的那个控件为止。
- 注意一个AccessibilityNodeInfo只代表一个控件,遍历的时候一旦需要下一个控件的信息是必须要再次通过UiAutomation去获取的。
## 2.触发控件查找真正发生的地方
在我没有去分析uiautomator的源代码之前,我一直以为空间查找是在通过UiSelector初始化一个UiObject的时候发生的:
~~~
UiObject appsTab = new UiObject(new UiSelector().text("Apps"));
~~~
这让我有一种先入为主的感觉,一个控件对象初始化好后应该就已经得到了该控件所代表的节点的所有信息了,但看了源码后发现事实并非如此,以上所做的事情只是以一定的格式准备好UiSelector选择子而已,真正触发uiautomator去获取控件节点信息的是在触发控件事件的时候,比如:
~~~
appsTab.click()
~~~
我们进入到代表一个控件的UiObject对应的操作控件的方法去看下就清楚了,以上面的click为例:
~~~
/* */ public boolean click()
/* */ throws UiObjectNotFoundException
/* */ {
/* 389 */ Tracer.trace(new Object[0]);
/* 390 */ AccessibilityNodeInfo node = findAccessibilityNodeInfo(this.mConfig.getWaitForSelectorTimeout());
/* 391 */ if (node == null) {
/* 392 */ throw new UiObjectNotFoundException(getSelector().toString());
/* */ }
/* 394 */ Rect rect = getVisibleBounds(node);
/* 395 */ return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), this.mConfig.getActionAcknowledgmentTimeout());
/* */ }
~~~
正式290行的调用触发uiautomator去调用UiAutomation去获取到我们想要的控件节点AccessibilityNodeInfo信息的。
## 3.获得根节点
下面我们看下uiautomator是怎么去获取到代表窗口所有控件的根的Root Node的,我们进入UiObject的findAccessibilityNodeInfo这个方法:
~~~
/* */ protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout)
/* */ {
/* 164 */ AccessibilityNodeInfo node = null;
/* 165 */ long startMills = SystemClock.uptimeMillis();
/* 166 */ long currentMills = 0L;
/* 167 */ while (currentMills <= timeout) {
/* 168 */ node = getQueryController().findAccessibilityNodeInfo(getSelector());
/* 169 */ if (node != null) {
/* */ break;
/* */ }
/* */
/* 173 */ UiDevice.getInstance().runWatchers();
/* */
/* 175 */ currentMills = SystemClock.uptimeMillis() - startMills;
/* 176 */ if (timeout > 0L) {
/* 177 */ SystemClock.sleep(1000L);
/* */ }
/* */ }
/* 180 */ return node;
/* */ }
~~~
UiObject对象会首先去获得一个QueryController对象,然后调用该对象的findAccessibilityNodeInfo同名方法:
~~~
/* */ protected AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector, boolean isCounting)
/* */ {
/* 143 */ this.mUiAutomatorBridge.waitForIdle();
/* 144 */ initializeNewSearch();
/* */
/* 146 */ if (DEBUG) {
/* 147 */ Log.d(LOG_TAG, "Searching: " + selector);
/* */ }
/* 149 */ synchronized (this.mLock) {
/* 150 */ AccessibilityNodeInfo rootNode = getRootNode();
/* 151 */ if (rootNode == null) {
/* 152 */ Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search");
/* 153 */ return null;
/* */ }
/* */
/* */
/* 157 */ UiSelector uiSelector = new UiSelector(selector);
/* 158 */ return translateCompoundSelector(uiSelector, rootNode, isCounting);
/* */ }
/* */ }
~~~
这里做了两个重要的事情:
- 150行:通过调用getRootNode来获得根节点,这个就是我们这个章节的重点
- 158行:通过调用translateCompoundSelector来根据用户指定的UiSelector格式从上面获得根节点开始遍历窗口控件树,以获得我们的目标控件
好,我们继续往下进入getRootNode:
~~~
/* */ protected AccessibilityNodeInfo getRootNode()
/* */ {
/* 168 */ int maxRetry = 4;
/* 169 */ long waitInterval = 250L;
/* 170 */ AccessibilityNodeInfo rootNode = null;
/* 171 */ for (int x = 0; x < 4; x++) {
/* 172 */ rootNode = this.mUiAutomatorBridge.getRootInActiveWindow();
/* 173 */ if (rootNode != null) {
/* 174 */ return rootNode;
/* */ }
/* 176 */ if (x < 3) {
/* 177 */ Log.e(LOG_TAG, "Got null root node from accessibility - Retrying...");
/* 178 */ SystemClock.sleep(250L);
/* */ }
/* */ }
/* 181 */ return rootNode;
/* */ }
~~~
172调用的是UiAutomatorBridge对象的方法,通过我们上面的几篇文章我们知道UiAutomatorBridge提供的方法大部分都是直接调用UiAutomation的方法的,我们进去看看是否如此:
~~~
/* */ public AccessibilityNodeInfo getRootInActiveWindow() {
/* 66 */ return this.mUiAutomation.getRootInActiveWindow();
/* */ }
~~~
果不其然,最终简单明了的直接调用UiAutomation的getRootInActiveWindow来获得根AccessibilityNodeInfo.
## 4.遍历根节点获得选择子UiSelector指定的控件
如前所述,QueryController的方法findAccessibilityNodeInfo在获得根节点后下来做的第二个事情:
- 158行:通过调用translateCompoundSelector来根据用户指定的UiSelector格式从上面获得根节点开始遍历窗口控件树,以获得我们的目标控件
里面的算法细节我就不打算去研究了,里面考虑到选择子嵌套的情况,分析起来也比较费力,且了解了它的算法对我去立交uiautomator的运行原理并没有非常大的帮助,我只需要知道给定一棵树的根,然后制定了我想要的叶子的属性,那么我遍历整棵树肯定是可以找到我想要的那个/些满足要求的控件的。大家由兴趣了解其算法的话还是自行去研究吧。
## 5.最终还是通过坐标点来点击控件
上面UiObject的Click方法通过UiAutomation这个高大上的新框架获得了代表我们目标控件的AccessibilityNodeInfo后,跟着是不是就直接调用这个节点的Click方法进行点击了呢?其实不是的,首先AccessibilityNodeInfo并没有click这个方法,我们继续看代码:
~~~
/* */ public boolean click()
/* */ throws UiObjectNotFoundException
/* */ {
/* 389 */ Tracer.trace(new Object[0]);
/* 390 */ AccessibilityNodeInfo node = findAccessibilityNodeInfo(this.mConfig.getWaitForSelectorTimeout());
/* 391 */ if (node == null) {
/* 392 */ throw new UiObjectNotFoundException(getSelector().toString());
/* */ }
/* 394 */ Rect rect = getVisibleBounds(node);
/* 395 */ return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(), this.mConfig.getActionAcknowledgmentTimeout());
/* */ }
~~~
从395行可以看到,最终还是把控件节点的信息转换成控件的坐标点进行点击的,至于怎么点击,大家可以参照上一篇文章,无非就是通过建立一个runnable的线程进行点击事件的注入了
## 6.系列结语
UiAutomator源码分析这个系列到了这篇文章算是完结了,从启动运行,到核心的UiAutomatorBridge架构,到实例解剖,通过这些文章我相信大家已经很清楚uiautomator这个运用了UiAutomation框架与AccessibilityService通信的测试框架是怎么回事了,置于uiautomator那5个专供测试用例调用的类是怎么回事,网上可获得的信息不少,我这里就没有必要做从新造轮子的事情了,况且这些已经不是uiautomator这个框架的核心了,它们只是运用了UiAutomatorBridge这个核心的一些类而已。
';
UiAutomator源码分析之注入事件
最后更新于:2022-04-01 19:55:34
上一篇文章《[UiAutomator源码分析之UiAutomatorBridge框架](http://blog.csdn.net/zhubaitian/article/details/40539103)》中我们把UiAutomatorBridge以及它相关的类进行的描述,往下我们会尝试根据两个实例将这些类给串联起来,我准备做的是用如下两个很有代表性的实例:
- 注入事件
- 获取控件
这一篇文章我们会通过分析UiDevice的pressHome这个方法来分析UiAutomator是如何注入事件的,下一篇文章会描述如何获取控件,敬请期待。
# 1. UiObject.pressHome顺序图
首先我们看一下我手画的非规范的顺序图,从中我们可以看到pressHome这个动作究竟需要和多少个类进行交互,以及它们是怎么交互的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e29eb28c.jpg)
# 2.这些类是什么时候初始化的
在我们编写测试用例脚本的时候我们不会对以上所有的类进行初始化,包括UiObject对象都是通过直接在脚本中调用父类UiAutomationTestCase的getUiDevice()这个方法来获得的。其实这些都是在uiautomator运行时由RunTestCommand类的start()这个方法进行初始化的,具体请看《[UIAutomator源码分析之启动和运行](http://blog.csdn.net/zhubaitian/article/details/40535579)》的 3.6章节“初始化UiDevice和UiAutomationBridge“,这里就不做累述。我们这里会看下在初始化UiAutomatorBridge的时候是如何把QuneryControoler和InteractionController一并初始化了的,具体请看UiAutomatorBridge的构造函数:
~~~
/* */ UiAutomatorBridge(UiAutomation uiAutomation)
/* */ {
/* 48 */ this.mUiAutomation = uiAutomation;
/* 49 */ this.mInteractionController = new InteractionController(this);
/* 50 */ this.mQueryController = new QueryController(this);
/* */ }
~~~
# 3. 代码跟踪
首先看UiDevice的pressHome方法:
~~~
public boolean pressHome() {
218 Tracer.trace();
219 waitForIdle();
220 return getAutomatorBridge().getInteractionController().sendKeyAndWaitForEvent(
221 KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
222 KEY_PRESS_EVENT_TIMEOUT);
223 }
~~~
220行:
- 获得UiDevice对象保存的UiAutomatorBridge对象。着两个对象都是在运行时初始化的,不清楚的话请翻看上面提到的文章
- 通过UiAutomatorBridge对象获得上面章节初始化的InteractionController对象
- 调用InteractionController对象的sendKeyAndWaitForEvent方法,里面参数关键是第一个keycode和第二个eventType
- keycode:代表我们要注入的是按下哪个按键的事件,比如这里我们是KEYCODE_HOME
- eventType:代表我们注射了该事件后预期会获得窗口返回来的哪种AccessibilityEvent类型,比如我们这里是TYPE_WINDOW_CONTENT_CHANGE
进入InteractionController类的sendKeyAndWaitForEvent:
~~~
/* */ public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState, int eventType, long timeout)
/* */ {
/* 188 */ Runnable command = new Runnable()
/* */ {
/* */ public void run() {
/* 191 */ long eventTime = SystemClock.uptimeMillis();
/* 192 */ KeyEvent downEvent = new KeyEvent(eventTime, eventTime, 0, keyCode, 0, metaState, -1, 0, 0, 257);
/* */
/* */
/* 195 */ if (InteractionController.this.injectEventSync(downEvent)) {
/* 196 */ KeyEvent upEvent = new KeyEvent(eventTime, eventTime, 1, keyCode, 0, metaState, -1, 0, 0, 257);
/* */
/* */
/* 199 */ InteractionController.this.injectEventSync(upEvent);
/* */ }
/* */
/* */ }
/* 203 */ };
/* 204 */ return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout) != null;
/* */ }
~~~
代码中创建了一个Runnable的线程,线程里面run重写方法要做的事情就是去做注入事件的事情,那么为什么我们不直接去调用事件而需要创建一个线程了,这是因为我们在注入完事件之后还要去等待我们上面定义的预期的eventType是否有出现来判断我们的事件注入究竟是否成功,这个就是204行runAndWaitForEvents做的事情。但我们这里还是先看下线程中是如何注入事件的:
~~~
/* */ private boolean injectEventSync(InputEvent event) {
/* 655 */ return this.mUiAutomatorBridge.injectInputEvent(event, true);
/* */ }
~~~
再跟踪到UiAutomatorBridge对象:
~~~
/* */ public boolean injectInputEvent(InputEvent event, boolean sync) {
/* 70 */ return this.mUiAutomation.injectInputEvent(event, sync);
/* */ }
~~~
可以看到最终还是通过UiAutomation来注入事件的,和我们的预期是一致的。
我们继续看InteractionController中真正执行注入事件线程的runAndWaitForEvents方法:
~~~
/* */ private AccessibilityEvent runAndWaitForEvents(Runnable command, UiAutomation.AccessibilityEventFilter filter, long timeout)
/* */ {
/* */ try
/* */ {
/* 161 */ return this.mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter, timeout);
/* */ }
/* */ catch (TimeoutException e) {
/* 164 */ Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");
/* 165 */ return null;
/* */ } catch (Exception e) {
/* 167 */ Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e); }
/* 168 */ return null;
/* */ }
~~~
代码又跳到了UiAutomatorBridge这个类
~~~
/* */ public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command, UiAutomation.AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException
/* */ {
/* 104 */ return this.mUiAutomation.executeAndWaitForEvent(command, filter, timeoutMillis);
/* */ }
~~~
最终把要执行的runnable执行注入事件的线程command和我们预期事件发生后返回来的窗口事件filter以及超时timeoutMillis传进去,UiAutomation就会和AccessibilityService进行交互以注入事件并且等待预期AccessibilityEvent发生或者超时返回。至于UiAutomation是如何和AccessibilityService交互的,这就超出了这个系列文章的范畴了。也许今后有充裕的时间的话我们再来深入去了解分析它。
';
UiAutomator源码分析之UiAutomatorBridge框架
最后更新于:2022-04-01 19:55:32
上一篇文章《[UIAutomator源码分析之启动和运行](http://blog.csdn.net/zhubaitian/article/details/40535579)》我们描述了uitautomator从命令行运行到加载测试用例运行测试的整个流程,过程中我们也描述了UiAutomatorBridge这个类的重要性,说它相当于UiAutomation的代理(我们都知道UiAutomator是通过UiAutomation和AccessibilityService进行连接然后获取界面空间信息和注入事件的).那么今天开始我们就围绕这个类以及跟它有关系的类进行进一步的分析。
##1. UiAutomatorBridge框架
这一章节我们会先看UiAutomatorBridge的整体框架,往后我会编写其他文章通过一些具体的例子把它们串起来。因为我的mackbook pro上没有安装类图软件,所以下图是手画的
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e29b3344.jpg)
往下我们就去初步描述下UiAutomatorBridge跟每一个相关的类的关系。
## 2. UiAutomatorBridge与UiAutomation的聚合关系
UiAutomatorBridge拥有一个UiAutomation的成员变量,它们是聚合的关系,注意不是组合,因为UiAutomation不一定只能依赖UiAutomatorBridge而存在,我们上一章节的UiAutomatorTestRunner就拥有一个UiAutomation的成员变量。
一旦UiAutomator工具需要通过UiAutomatorBridge获取界面或者注入事件的时候,就会调用该成员变量.比如下面这个很关键的去获取当前界面的Root Node的方法:
~~~
/* */ public AccessibilityNodeInfo getRootInActiveWindow() {
/* 66 */ return this.mUiAutomation.getRootInActiveWindow();
/* */ }
~~~
## 3. UiAutomatorBridge与QueryController的关联关系
QueryController做的所有事情就是去把UiSelector这个UI控件选择子翻译成真实的适合我们使用的android.view.accessibility.AccessibilityNodeInfo。 UiAutomatorBridge拥有一个成员变量mQueryController保存了QueryController的一个实例:
~~~
/* */ private final QueryController mQueryController;
/* */
~~~
当UiObject需要获取一个UiSelector指定的控件信息时,会去调用UiAutomatorBridge的getQueryController方法来获得这个mQueryController对象来进行相应的操作,如以下的UiObject的方法findAccessibilityNodeInfo就是这样做的:
~~~
/* */ protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout)
/* */ {
/* 164 */ AccessibilityNodeInfo node = null;
/* 165 */ long startMills = SystemClock.uptimeMillis();
/* 166 */ long currentMills = 0L;
/* 167 */ while (currentMills <= timeout) {
/* 168 */ node = getQueryController().findAccessibilityNodeInfo(getSelector());
/* 169 */ if (node != null) {
/* */ break;
/* */ }
/* */
/* 173 */ UiDevice.getInstance().runWatchers();
/* */
/* 175 */ currentMills = SystemClock.uptimeMillis() - startMills;
/* 176 */ if (timeout > 0L) {
/* 177 */ SystemClock.sleep(1000L);
/* */ }
/* */ }
/* 180 */ return node;
/* */ }
~~~
该getQueryController方法会去调用UiAutomatorBridge的getQueryController方法:
~~~
/* */ QueryController getQueryController()
/* */ {
/* 100 */ return UiDevice.getInstance().getAutomatorBridge().getQueryController();
/* */ }
~~~
从上面的类图我们可以看到,除了UiAutomatorBridge会调用QueryController做事情外,QueryController又会反过来调用UiAutomatorBridge来做事情,因为如图所描述的,只有UiAutomatorBridge拥有UiAutomation的实例,所以QueryController会持有一个UiAutomatorBridge的实例:
~~~
/* */ private final UiAutomatorBridge mUiAutomatorBridge;
~~~
然后在需要的时候再调用UiAutomatorBridge,如下面的获得Root Node的方法:
~~~
/* */ protected AccessibilityNodeInfo getRootNode()
/* */ {
/* 168 */ int maxRetry = 4;
/* 169 */ long waitInterval = 250L;
/* 170 */ AccessibilityNodeInfo rootNode = null;
/* 171 */ for (int x = 0; x < 4; x++) {
/* 172 */ rootNode = this.mUiAutomatorBridge.getRootInActiveWindow();
/* 173 */ if (rootNode != null) {
/* 174 */ return rootNode;
/* */ }
/* 176 */ if (x < 3) {
/* 177 */ Log.e(LOG_TAG, "Got null root node from accessibility - Retrying...");
/* 178 */ SystemClock.sleep(250L);
/* */ }
/* */ }
/* 181 */ return rootNode;
/* */ }
~~~
## 4. UiAutomatorBridge与InteractionController的关联关系
道理与以上的QueryController一样,只是UiAutomatorBridge需要通过InteractionController做的事情不是去获得控件信息,而是去注入事件。
## 5. UiAutomatorBridge与ShellUiAutomatorBridge的继承关系
UiAutomatorBridge是一个抽象类,里面的方法有以下几个:
- getRootInActiveWindow:通过UiAutomation获取当前窗口控件xml信息的根节点(通过它可以循环获取所有控件)
- injectInputEvent:通过UiAutomation注入事件
-
waitForIdle: 通过UiAutomation睡眠指定时间
-
executeCommandAndWaitForAccessibilityEvent:通过UiAutomation执行指定线程的操作然后等待预期的时间返回
-
takeScreenshot:通过UiAutomation进行截图
-
performGlobalAction: 通过UiAutomation去执行一些全局的动作,如打开最近打开过的app列表,回到home界面等
从中可以看到这些动过都是需要通过UiAutomation来执行的,但也有一些动作是不需要用UiAutomation执行的,所以我相信google是为了代码清晰和可维护性,提供了子类ShellUiAutomatorBridge来专门处理那些不需要用到UiAutomation的情况,比如以下的isScreenOn方法就不需要用到UiAutomation,而是直接用PowerManager服务来判断当前屏幕是否是打开的:
~~~
/* */ public boolean isScreenOn()
/* */ {
/* 111 */ IPowerManager pm = IPowerManager.Stub.asInterface(ServiceManager.getService("power"));
/* */
/* 113 */ boolean ret = false;
/* */ try {
/* 115 */ ret = pm.isScreenOn();
/* */ } catch (RemoteException e) {
/* 117 */ Log.e(LOG_TAG, "Error getting screen status", e);
/* 118 */ throw new RuntimeException(e);
/* */ }
/* 120 */ return ret;
/* */ }
~~~
';
UIAutomator源码分析之启动和运行
最后更新于:2022-04-01 19:55:30
通过上一篇《[Android4.3引入的UiAutomation新框架官方简介](http://blog.csdn.net/zhubaitian/article/details/40504827)》我们可以看到UiAutomator其实就是使用了UiAutomation这个新框架,通过调用AccessibilitService APIs来获取窗口界面控件信息已经注入用户行为事件,那么今天开始我们就一起去看下UiAutomator是怎么运作的。
我们在编写了测试用例之后,我们需要通过以下几个步骤把测试脚本build起来并放到测试机器上面:
- android create uitest-project -n AutoRunner.jar -t 5 -p D:\\Projects\UiAutomatorDemo
- adb push e:\workspace\AutoRunner\bin\AutoRunner.jar data/local/tmp
然后通过以下命令把测试运行起来:
- adb shell uiautomator runtest AutoRunner.jar -c majcit.com.UIAutomatorDemo.SettingsSample
那么我们就围绕以上这个命令,从uiautomator这个命令作为突破口,看它是怎么跑起来的。开始之前我们先看下uiautomator的help帮助:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e297176f.jpg)
- 支持三个子命令:rutest/dump/events
- runtest命令-c指定要测试的class文件,用逗号分开,没有指定的话默认执行测试脚本jar包的所有测试类.注意用户可以以格式$class/$method来指定只是测试该class的某一个指定的方法
- runtest命令-e参数可以指定是否开启debug模式
- runtest命令-e参数可以指定test runner,不指定就使用系统默认。我自己从来没有指定过
- runtest命令-e参数还可以通过键值对来指定传递给测试类的参数
同时我们这里会涉及到几个重要的类,我们这里先列出来给大家有一个初步的印象:
Class | Package | Description |
Launcher | com.android.commands.uiautomator | uiautomator命令的入口方法main所在的类 |
RunTestCommand | com.android.commands | 代表了命令行中‘uiautomator runtest'这个子命令 |
EventsCommand | com.android.commands | 代表了命令行中‘uiautomator events’这个子命令 |
DumpCommand | com.android.commands | 代表了命令行中‘uiautomator dump’这个子命令 |
UIAutomatorTestRunner | com.android.uiautomator.testrunner | 默认的TestRunner,用来知道测试用例如何执行 |
TestCaseCollector | com.android.uiautomator.testrunner | 用来从命令行和我们的测试脚本.class文件收集每个测试方法然后建立对应的junit.framework.TestCase测试用例的一个类,它维护着一个List<TestCase> mTestCases列表来存储所有测试方法(用例) |
UiAutomationShellWrapper | com.android.uiautomator.core | 一个UiAutomation的wrapper类,简单的做了封装,其中提供了一个setRunAsMonkey的方法来通过ActivityManagerNativeProxy来设置系统的运行模式 |
UiAutomatorBridge | com.android.uiautomator.core | 相当于UiAutomation的代理,基本上所有和UiAutomation打交道的方法都是通过它来分发的 |
ShellUiAutomatorBridge | com.android.uiautomator.core | UiAutomatorBridge的子类,额外增加了几个不需要用到UiAutomation的方法,如getRotation |
# 1.环境变量配置
和monkey以及monkeyrunner一样,uiautomator其实也是一个shell脚本,我们看最后面的关键几行:
~~~
CLASSPATH=${CLASSPATH}:${jars}
export CLASSPATH
exec app_process ${base}/bin com.android.commands.uiautomator.Launcher ${args}
~~~
我们先把这些变量打印出来,看都是些什么值:
- **CLASSPATH**:/system/framework/android.test.runner.jar:/system/framework/uiautomator.jar::/data/local/tmp/AutoRunner.jar
- **base**:/system
- **${args}**:runtest -c majcit.com.UIAutomatorDemo.SettingsSample -e jars :/data/local/tmp/AutoRunner.jar
如monkey一样,这个shell脚本会:
- 首先export需要的classpath环境变量,让我们的脚本用到的jar包可以在目标设备上被正常的引用到(毕竟我们在客户端开发的时候引用到的jar包是本地的,比如uiautomator.jar这个jar包。
- 然后通过app_process来指定命令工作路径为'/system/bin/'以启动指定类com.android.commands.uiautomator.Launcher,启动该类传入的参数就是我们指定的测试用例类和我们build好的测试脚本jar包:runtest -c majcit.com.UIAutomatorDemo.SettingsSample -e jars :/data/local/tmp/AutoRunner.jar
那么现在我们就知道我们的入口就在com.android.commands.uiautomator.Launcher这个class里面了。
## 2. 子命令定位
打开com.android.commands.uiautomator.Launcher这个类的原文件,我们首先定位它的入口函数main:
~~~
/* */ public static void main(String[] args)
/* */ {
/* 74 */ Process.setArgV0("uiautomator");
/* 75 */ if (args.length >= 1) {
/* 76 */ Command command = findCommand(args[0]);
/* 77 */ if (command != null) {
/* 78 */ String[] args2 = new String[0];
/* 79 */ if (args.length > 1)
/* */ {
/* 81 */ args2 = (String[])Arrays.copyOfRange(args, 1, args.length);
/* */ }
/* 83 */ command.run(args2);
/* 84 */ return;
/* */ }
/* */ }
/* 87 */ HELP_COMMAND.run(args);
/* */ }
~~~
里面主要做两件事情:
- 76行:根据输入的第一个参数查找到Command,在我们的例子中第一个参数是runtest,所以要找到的就是runtest这个命令对应的Command
- 83行:执行查找到的command的run方法开始执行测试
那么到了这里我们首先要搞清楚Command是怎么一回事。其实说白了一个Command就代表了我们命令行调用uiautomator输入的第一个参数,也就是subcommand,比如我们这里就是runtest这一个命令,如果用户输入的是'uiautomator dump'去尝试dump一个当前窗口界面的所有空间信息,那么该command就代表了dump这一个命令。uiautomator总共支持3种command(不连help):
- **runtest**:对应RunTestCommand这个类,代表运行相应测试的命令
- **dump**: 对应DumpCommand这个类,dump当前窗口控件信息,你在命令行运行‘uiautomator dump’就会把当前ui的hierarchy信息dump成一个文件默认放到sdcard上
- **events**: 对应EventsCommand这个类,获取accessibility events,你在命令行运行'uiautomator events'然后在链接设备上操作一下就会看到相应的事件打印出来
在Launcher里面有一个静态预定义列表COMMANDS定义了这些Command:
~~~
/* 129 */ private static Command[] COMMANDS = { HELP_COMMAND, new RunTestCommand(), new DumpCommand(), new EventsCommand() };
~~~
这些命令,如我们的RunTestCommand类都是继承与Command这个Launcher的静态抽象内部类:
~~~
/* */ public static abstract class Command
/* */ {
/* */ private String mName;
/* */
/* */ public Command(String name)
/* */ {
/* 40 */ this.mName = name;
/* */ }
/* */ public String name()
/* */ {
/* 48 */ return this.mName;
/* */ }
/* */
/* */ public abstract String shortHelp();
/* */ public abstract String detailedOptions();
/* */
/* */ public abstract void run(String[] paramArrayOfString);
/* */ }
~~~
里面定义了一个mName的字串成员,其实对应的就是我们命令行传进来的第一个参数,大家看下子类RunTestCommand这个类的构造函数就清楚了:
~~~
/* */ public RunTestCommand() {
/* 62 */ super("runtest");
/* */ }
~~~
然后Command类还定义了一个run的方法,注意这个方法非常重要,这个就是我们刚才分析main函数看到的第二点,是开始运行测试的地方。
好,我们返回之前的main方法,看是怎么根据‘runtest'这个我们输入的字串找到对应的RunTestCommand这个command的,我们打开findCommand这个方法:
~~~
/* */ private static Command findCommand(String name) {
/* 91 */ for (Command command : COMMANDS) {
/* 92 */ if (command.name().equals(name)) {
/* 93 */ return command;
/* */ }
/* */ }
/* 96 */ return null;
/* */ }
~~~
跟我们预期一样,该方法就是循坏COMMANDS这个预定义的静态command列表,把上面提到的它们的nName取出来比较,然后找到对应的command对象的。
## 3. 准备运行
在获取到我们对应的命令之后,下一步我们就需要根据命令行传进来的参数来设置我们对应的command对象,以RunTestCommand为例,从main方法进入到run:
~~~
/* */ public void run(String[] args)
/* */ {
/* 67 */ int ret = parseArgs(args);
...
/* 84 */ if (this.mTestClasses.isEmpty()) {
/* 85 */ addTestClassesFromJars();
/* 86 */ if (this.mTestClasses.isEmpty()) {
/* 87 */ System.err.println("No test classes found.");
/* 88 */ System.exit(-3);
/* */ }
/* */ }
/* 91 */ getRunner().run(this.mTestClasses, this.mParams, this.mDebug, this.mMonkey);
/* */ }
~~~
这里做了几个事情:
- 67行:根据命令行参数设置RunTestCommand的命令属性
- 84-85行:如果没有-c参数指定测试类或者指定-e class,那么默认从指定的jar包里面获取所有的测试class进行测试
- 91行:获取testrunner并执行run方法
### 3.1 设置命令运行参数
我们进入parseArgs里面看RunTestCommand是如何根据命令行参数来设置相应的变量的:
~~~
/* */ private int parseArgs(String[] args)
/* */ {
/* 105 */ for (int i = 0; i < args.length; i++) {
/* 106 */ if (args[i].equals("-e")) {
/* 107 */ if (i + 2 < args.length) {
/* 108 */ String key = args[(++i)];
/* 109 */ String value = args[(++i)];
/* 110 */ if ("class".equals(key)) {
/* 111 */ addTestClasses(value);
/* 112 */ } else if ("debug".equals(key)) {
/* 113 */ this.mDebug = (("true".equals(value)) || ("1".equals(value)));
/* 114 */ } else if ("runner".equals(key)) {
/* 115 */ this.mRunnerClassName = value;
/* */ } else {
/* 117 */ this.mParams.putString(key, value);
/* */ }
/* */ } else {
/* 120 */ return -1;
/* */ }
/* 122 */ } else if (args[i].equals("-c")) {
/* 123 */ if (i + 1 < args.length) {
/* 124 */ addTestClasses(args[(++i)]);
/* */ } else {
/* 126 */ return -2;
/* */ }
/* 128 */ } else if (args[i].equals("--monkey")) {
/* 129 */ this.mMonkey = true;
/* 130 */ } else if (args[i].equals("-s")) {
/* 131 */ this.mParams.putString("outputFormat", "simple");
/* */ } else {
/* 133 */ return -99;
/* */ }
/* */ }
/* 136 */ return 0;
/* */ }
~~~
- 106-117行:判断是否有-e参数,有指定debug的话就启动debug;有指定runner的就设置runner;有指定class的话就通过addTestClasses把该测试脚本类加入到mTestClasses列表;有指定其他键值对的就保存起来到mParams这个map里面,比如我们例子种是没有指定debug和runner,但shell脚本自动会通过-e加上一个键值为jars的键值对,值就是我们的测试脚本jar包存放的路径
- 122-129行:判断是否有-c参数,有的话就把对应的class加入到RunTestCommand对象的mTestClasses这个列表里面,注意每个class需要用逗号分开:
~~~
/* */ private void addTestClasses(String classes)
/* */ {
/* 181 */ String[] classArray = classes.split(",");
/* 182 */ for (String clazz : classArray) {
/* 183 */ this.mTestClasses.add(clazz);
/* */ }
/* */ }
~~~
- 其他参数处理...
### 3.2 获取测试集(类)字串列表
处理好命令行参数后RunTestCommand的run方法下一个做的事情就是检查mTestClasses这个字串类型列表是空的,根据上面的parseArgs方法的分析,如果命令行没有指定-c或者没有指定-e class,那么这个mTestClasses就为空,这种情况下就会把我们通过adb push进来的命令脚本jar包中的所有class加入到mTestClasses这个字串列表中,也就是说会执行里面的所有脚本。
### 3.3 获取TestRunner
准备好命令参数和要执行的测试类后,下一步就要获取对应的TestRunner来指导测试脚本的执行了,我们看下我们是怎么获得TestRunner的:
~~~
/* */ protected UiAutomatorTestRunner getRunner() {
/* 140 */ if (this.mRunner != null) {
/* 141 */ return this.mRunner;
/* */ }
/* */
/* 144 */ if (this.mRunnerClassName == null) {
/* 145 */ this.mRunner = new UiAutomatorTestRunner();
/* 146 */ return this.mRunner;
/* */ }
/* */
/* 149 */ Object o = null;
/* */ try {
/* 151 */ Class> clazz = Class.forName(this.mRunnerClassName);
/* 152 */ o = clazz.newInstance();
/* */ } catch (ClassNotFoundException cnfe) {
/* 154 */ System.err.println("Cannot find runner: " + this.mRunnerClassName);
/* 155 */ System.exit(-4);
/* */ } catch (InstantiationException ie) {
/* 157 */ System.err.println("Cannot instantiate runner: " + this.mRunnerClassName);
/* 158 */ System.exit(-4);
/* */ } catch (IllegalAccessException iae) {
/* 160 */ System.err.println("Constructor of runner " + this.mRunnerClassName + " is not accessibile");
/* 161 */ System.exit(-4);
/* */ }
/* */ try {
/* 164 */ UiAutomatorTestRunner runner = (UiAutomatorTestRunner)o;
/* 165 */ this.mRunner = runner;
/* 166 */ return runner;
/* */ } catch (ClassCastException cce) {
/* 168 */ System.err.println("Specified runner is not subclass of " + UiAutomatorTestRunner.class.getSimpleName());
/* */
/* 170 */ System.exit(-4);
/* */ }
/* */
/* 173 */ return null;
/* */ }
~~~
这个类看上去有点长,但其实做的事情重要的就那么两点,其他的都是些错误处理:
- 用户有没有在命令行通过-e runner指定TestRunner,有的话就用该TestRunner
- 用户没有指定TestRunner的话就用默认的UiAutomatorTestRunner
### 3.4 每个方法建立junit.framework.TestCase
确定了UiAutomatorTestRunner这个TestRunner后的下一步就是调用它的run方法来指导测试用例的执行:
~~~
/* */ public void run(List
testClasses, Bundle params, boolean debug, boolean monkey)
/* */ {
...
/* 92 */ this.mTestClasses = testClasses;
/* 93 */ this.mParams = params;
/* 94 */ this.mDebug = debug;
/* 95 */ this.mMonkey = monkey;
/* 96 */ start();
/* 97 */ System.exit(0);
/* */ }
~~~
传进来的参数就是我们刚才通过parseArgs方法设置的那些变量,run方法会把这些变量保存起来以便下面使用,紧跟着它就会调用一个**start**方法,这个方法非常重要,从建立每个测试方法对应的junit.framwork.TestCase对象到真正执行测试都在这个方法完成,所以也比较长,我们挑重要的部分进行分析,首先我们看以下代码:
~~~
/* */ protected void start()
/* */ {
/* 104 */ TestCaseCollector collector = getTestCaseCollector(getClass().getClassLoader());
/* */ try {
/* 106 */ collector.addTestClasses(this.mTestClasses);
/* */ }
...
}
~~~
这里面调用了TestCaseCollector这个类的addTestClasses的方法,从这个类的名字我们可以猜测到它就是专门收集测试用例用的,那么我们往下跟踪下看它是怎么收集测试用例的:
~~~
/* */ public void addTestClasses(List classNames)
/* */ throws ClassNotFoundException
/* */ {
/* 52 */ for (String className : classNames) {
/* 53 */ addTestClass(className);
/* */ }
/* */ }
~~~
这里传进来的就是我们上面保存起来的收集了每个class名字的字串列表。里面执行了一个for循环来把每一个类的字串拿出来,然后调用addTestClass:
~~~
/* */ public void addTestClass(String className)
/* */ throws ClassNotFoundException
/* */ {
/* 66 */ int hashPos = className.indexOf('#');
/* 67 */ String methodName = null;
/* 68 */ if (hashPos != -1) {
/* 69 */ methodName = className.substring(hashPos + 1);
/* 70 */ className = className.substring(0, hashPos);
/* */ }
/* 72 */ addTestClass(className, methodName);
/* */ }
~~~
这里可能你会奇怪为什么会查看类名字串里面是否有#号呢?其实在文章开头的时候我就有提出来,-c或者-e class指定的类名是可以支持 $className/$methodName来指定执行该className的methodName这个方法的,比如我可以指定-c majcit.com.UIAutomatorDemo.SettingsSample#testSetLanEng来指定只是测试该类里面的testSetLanEng这个方法。如果用户没有指定的话该methodName变量就设置成null,然后调用重载方法addTestClass方法:
~~~
/* */ public void addTestClass(String className, String methodName)
/* */ throws ClassNotFoundException
/* */ {
/* 84 */ Class> clazz = this.mClassLoader.loadClass(className);
/* 85 */ if (methodName != null) {
/* 86 */ addSingleTestMethod(clazz, methodName);
/* */ } else {
/* 88 */ Method[] methods = clazz.getMethods();
/* 89 */ for (Method method : methods) {
/* 90 */ if (this.mFilter.accept(method)) {
/* 91 */ addSingleTestMethod(clazz, method.getName());
/* */ }
/* */ }
/* */ }
/* */ }
~~~
- 84行:最终会调用 java.lang.ClassLoader的loadClass方法,通过指定类的名字来把该测试脚本类装载进来并赋予给clazz这个Class>变量,注意这里这个测试类还没有实例化的,真正实例化的地方是在下面的addSingleTestMethod中
- 85-86行:如果用户用#号指定测试某一个类的某个方法,那么就直接传入参数clazz和要测试的methodName来调用addSingleTestMehod来组建我们需要的TestCase
- 88-91行:如果用户没用#号指定测试某个类的某个方法,那么就需要循环取出该类的所有测试方法,然后每个方法调用一次addSingleTestMethod.
好,终于来到的关键点,下面我们看addSingleTestMethod是如何根据测试类clazz和它的一个方法创建一个junit.framework.TestCase对象的:
~~~
/* */ protected void addSingleTestMethod(Class> clazz, String method) {
/* 106 */ if (!this.mFilter.accept(clazz)) {
/* 107 */ throw new RuntimeException("Test class must be derived from UiAutomatorTestCase");
/* */ }
/* */ try {
/* 110 */ TestCase testCase = (TestCase)clazz.newInstance();
/* 111 */ testCase.setName(method);
/* 112 */ this.mTestCases.add(testCase);
/* */ } catch (InstantiationException e) {
/* 114 */ this.mTestCases.add(error(clazz, "InstantiationException: could not instantiate test class. Class: " + clazz.getName()));
/* */ }
/* */ catch (IllegalAccessException e) {
/* 117 */ this.mTestCases.add(error(clazz, "IllegalAccessException: could not instantiate test class. Class: " + clazz.getName()));
/* */ }
/* */ }
~~~
- 106-107行:这一个判断非常的重要,我们的测试脚本必须都是继承于UiAutomatorTestCase的,否则不支持!
- 110行:把测试用例类进行初始化获得一个实例对象,然后强制转换成junit.framework.TestCase类型,这里要注意我们测试脚本的父类UiAutomationTestCase也是继承与junit.framework.TestCase的
- 111行:设置junit.framework.TestCase实例对象的方法名字,这个很重要,下一章节可以看到junit框架会通过它来找到我们测试脚本中要执行的那个方法
- 112行:把这个TestCase对象增加到当前TestCaseCollector的mTestCases这个junit.framework.TestCase类型的列表里面
这个小节代码稍微多了点,其实简单来说就是UiAutomatorTestRunner在指导测试用例怎么跑的时候,会去请求TestCaseController去把用户传进来的测试类名字字串列表中的每个类对应的每个方法转换成junit.framework.TestCase,并把这些TestCase保存在TestCaseCollector对象的**mTestCases**这个列表里面。
这里千万要注意的一点是;**并非一个测试脚本(类)一个TestCase,而是一个方法创建一个TestCase!**
### 3.5 初始化UiAutomationShellWrapper并连接上AccessibilityService来设置Monkey模式
上面UiAutomatorTestRunner的start方法在调用完TestCaseCollector来建立TestCase列表后,会尝试建立AccessibilityService的连接,来看是否应该把UiAutomation设置成Monkey运行模式:
~~~
/* */ protected void start()
/* */ {
...
/* 117 */ UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
/* 118 */ automationWrapper.connect();
/* */
...
/* */ try {
/* 132 */ automationWrapper.setRunAsMonkey(this.mMonkey);
...
}
...
}
~~~
这里会初始化一个UiAutomationShellWrapper的类,其实这个类如其名,就是UiAutomation的一个Wrapper,初始化好后最终会调用UiAutomation的connect方法来连接上AccessibilityService服务,然后就可以调用AccessibilityService相应的API来把UiAutomation设置成Monkey模式来运行了。而在我们的例子中我们没有指定monkey模式的参数,所以是不会设置monkey模式的。
至于什么是Monkey模式,我说了不算,官方说了算:
*Applications can query whether they are executed in a "monkey" mode, i.e. run by a test framework, and avoid doing potentially undesirable actions such as calling 911 or posting on public forums etc.*
也就是说设置了这个模式之后,一些应用会调用我们《[Android4.3引入的UiAutomation新框架官方简介](http://blog.csdn.net/zhubaitian/article/details/40504827)》提到的isUserMonkey()这个著名的api来判断究竟是不是一个测试脚本在要求本应用做事情,那么判断如果是的话就不要让它做一些意想不到的如拨打911的事情。不然你一个测试脚本写错了,一个死循环一个晚上在拨打911,保管警察第二天上你公司找你。
### 3.6 初始化UiDevice和UiAutomationBridge
在所有要运行的基于每个方法的TestCase都准备好之后,我们还不能直接去调用junit.framework.TestCase的run方法来执行该方法,我们还需要做几个很重要的事情:
- 初始化一个UiDevice对象
- 每执行一个测试方法之前必须给该脚本传入该UiDevice对象。大家写过UiAutomator脚本的应该都知道UiDevce不是调用构造函数而是通过getUiDevice获得的,而getUiDevice其实就是我们的测试脚本的父类UiAutomatorTestCase的方法,往后我们会看到它们是怎么联系起来的
好,我们继续分析上面UiAutomatorTestRunner的start方法,上面一小节它完成了测试用例每个方法对应的junit.framework.TestCase对象的建立,那么往下:
~~~
/* */ protected void start()
/* */ {
...
/* */ try {
/* 132 */ automationWrapper.setRunAsMonkey(this.mMonkey);
/* 133 */ this.mUiDevice = UiDevice.getInstance();
/* 134 */ this.mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation()));
/* */
...
}
...
}
~~~
在尝试设置monkey模式之后,UiAutomatorTestRunner会去实例化一个UiDevice,实例化后会通过以下步骤对其进行初始化:
- 首先获取上一小节提到的UiAutomationShellWrapper这个Wrapper里面的UiAutomation实例,注意这个实例在上一小节中已经连接上AccessiblityService的了
- 以这个连接好的UiAutomation为参数构造一个ShellUiAutomatorBridge,注意这里不是UiAutiomatorBridge。ShellUiAutomatorBridge时继承于UiAutomatorBridge的一个子类,里面实现了额外的几个不需要通过UiAutomation的操作,比如getRotation等是通过WindowManager来实现的
- 最后通过调用UiDevice的initialize这个方法传入ShellUiAutomatorBridge的实例来初始化我们的UiDevice
- 完成以上的初始化后,我们就拥有了一个已经通过UiAutomation连接上设备的AccessibilityService的UiDevice了,这样我们就可以随意调用AccessibilityService API来为我们服务了
这里提到的一些类也许对你会有点陌生,本人接下来会另外开文章去进行描述。
## 4. 启动junit测试
到现在位置似乎所有东西都准备好了:
- 每个测试用例中的每个测试方法对应的junit.framework.TestCase建立好
- 已经连接上AccessibilityService的UiDevice准备好
那么我们是不是就可以立刻直接调用junit.framework.TestCase的run开始执行测试方法呢?既然以这种调调来提问,答案可想而知肯定不是的了。那么为什么还不能运行呢?既然这些都准备好了。其实这里问题是UiDevice,确实,上面的UiDevice实例已经拥有一个UiAutomation对象,且该对象已经连接上AccessibilityService服务,但是你要知道这个UiDevice对象现在是UiAutomatorTestRunner这个类的对象拥有的,而我们的测试脚本并没有继承或者拥有这个类的变量。请看以下的测试脚本:
~~~
package majcit.com.UIAutomatorDemo;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
public class UISelectorFindElementTest extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
UiDevice device = getUiDevice();
device.pressHome();
~~~
既然测试脚本中的getUiDevice方法不是直接从UiAutomatorTestRunner获得,那么是不是从它继承下来的UiAutomatorTestCase中获得呢?答案是肯定的,我们继续看那个UiAutomatorTestRunner中很重要的start方法:
~~~
/* */
/* */ protected void start()
/* */ {
...
/* 158 */ for (TestCase testCase : testCases) {
/* 159 */ prepareTestCase(testCase);
/* 160 */ testCase.run(testRunResult);
/* */ }
...
}
~~~
一个for循环把我们上面创建好的所有junit.framework.TestCase对象做一个遍历,在执行之前先调用一个prepareTestCase:
~~~
/* */ protected void prepareTestCase(TestCase testCase)
/* */ {
/* 427 */ ((UiAutomatorTestCase)testCase).setAutomationSupport(this.mAutomationSupport);
/* 428 */ ((UiAutomatorTestCase)testCase).setUiDevice(this.mUiDevice);
/* 429 */ ((UiAutomatorTestCase)testCase).setParams(this.mParams);
/* */ }
~~~
这个方法所做的事情就解决了我们刚才的疑问:第428行,把当前UiAutomatorTestRunner拥有的这个已经连接到AccessibilityService的UiObject对象,通过我们测试脚本的父类的setUiDevice方法设置到我们的TestCase脚本对象里面
~~~
/* */ void setUiDevice(UiDevice uiDevice)
/* */ {
/* 100 */ this.mUiDevice = uiDevice;
/* */ }
~~~
这样我们测试脚本每次执行getUiDevice的时候就能直接取得该对象了:
~~~
/* */ public UiDevice getUiDevice()
/* */ {
/* 72 */ return this.mUiDevice;
/* */ }
~~~
从整个过程可以看到,UiObject的对象我们在测试脚本上是不用初始化的,它是在运行时由我们默认的TestuRunner -- UiAutomatorTestRunner 传递进来的,这个我们作为测试人员是不需要知道这一点的。
好了,到了现在就真的可以直接触发junit.framework.TestCase的run方法来让测试跑起来了,这里要注意我们之前的分析,并不是测试脚本的所有方法都同时调用run执行的,而是一个方法调用一次run方法。
## 5. 扩展阅读:junit框架如何通过方法名执行测试方法
下面如果有兴趣知道juint框架是如何通过3.4节建立junit.framework.TestCase时调用setName方法设置的测试方法名字来调用执行对应方法的可以继续往下跟踪run方法,它最终会进入到junit.framework.TestCase的runTest方法
~~~
protected void runTest() throws Throwable {
assertNotNull(fName); // Some VMs crash when calling getMethod(null,null);
Method runMethod= null;
try {
// use getMethod to get all public inherited
// methods. getDeclaredMethods returns all
// methods of this class but excludes the
// inherited ones.
runMethod= getClass().getMethod(fName, (Class[])null);
} catch (NoSuchMethodException e) {
fail("Method \""+fName+"\" not found");
}
if (!Modifier.isPublic(runMethod.getModifiers())) {
fail("Method \""+fName+"\" should be public");
}
try {
runMethod.invoke(this, (Object[])new Class[0]);
}
catch (InvocationTargetException e) {
e.fillInStackTrace();
throw e.getTargetException();
}
catch (IllegalAccessException e) {
e.fillInStackTrace();
throw e;
}
}
~~~
从中可以看到它会尝试通过getClass().getMethod方法获得这个junit.framework.TestCase所代表的测试脚本的于我们设置的fName一致的方法,然后才会去执行。
';
UIAutomator定位Android控件的方法实践和建议(Appium姊妹篇)
最后更新于:2022-04-01 19:55:27
在本人之前的一篇文章<<[Appium基于安卓的各种FindElement的控件定位方法实践和建议](http://blog.csdn.net/zhubaitian/article/details/39754041)>>第二章节谈到Appium可以通过使用UIAutomator的方法去定位Android界面上的控件,当时只是一笔带过举了个例子。如该文给自己的承诺,今天特撰写此文以描述UIAutomator各种控件定位的方法,以作为前文的姊妹篇互通有无。
## 1. 背景
为了和前文达成一致,这次的实践对象同样也是使用SDK自带的NotePad应用,同样是尝试去获得在NotesList那个Activity里的Menu Options上面的那个Add note菜单选项。以下是UIAutomatorViewer对界面的一个截图.
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e2928ee1.jpg)
但有一个例外的地方是下文的”*通过伪xpath方法定位控件*“章节实例需要使用到的是NoteEditor这个activity里面的Menu options,因为需要演示通过子控件获得父控件然后得到兄弟控件的功能,UIAutomatorViewer截图如下。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e294d1d7.jpg)
## 2. 通过文本信息定位
通过控件的text属性定位控件应该是最常用的一种方法了,毕竟移动应用的屏幕大小有限,存在text重复的可能性并不大,就算真的有重复,可以添加其他定位方法来缩写误差。
### 2.1 UISelector.text方法
~~~
addNote = new UiObject(new UiSelector().text("Add note"));
assertEquals(addNote.getText(),"Add note");
~~~
该方法通过直接查找当前界面上所有的控件来比较每个控件的text属性是否如预期值来定位控件,挺好理解的,所以就没有必要细说了。
### 2.2. UISelector.textContains方法
~~~
addNote = new UiObject(new UiSelector().textContains("Add"));
assertEquals(addNote.getText(),"Add note");
~~~
此方法跟以上方法类似,但是不需要输入控件的全部text信息。
### 2.3 UISelector.textStartsWith方法
~~~
addNote = new UiObject(new UiSelector().textStartsWith("Add"));
assertEquals(addNote.getText(),"Add note");
~~~
顾名思义,通过判断一个控件的text的开始是否和预期的字串相吻合来获得控件,其实个人觉得这个方法存在的必要性不强,因为它的功能完全可以用上面的方法或者下面的正则表达式的方法取代。况且既然你提供了textStartsWith方法,为什么你不提供个textEndWith的方法呢!
### 2.4 UISelector.textMatches方法
~~~
addNote = new UiObject(new UiSelector().textMatches("^Add.*"));
assertEquals(addNote.getText(),"Add note");
~~~
这个方法是通过正则表达式的方式来比较控件的text来定位控件,这里有意思的是用户使用的正则表达式是有限制的,请看该方法的官方描述:”*Set the search criteria to match the visible text displayed for a widget (for example, the text label to launch an app). The text for the widget must match exactly with the string in your input argument*“。第一句我们不用管它,关键是第二句,翻译过来就是”目标控件的text(的所有内容)必须和我们输入的正则表达式完全匹配“。什么意思呢?意思就是你不能像往常的正则表达式那样通过比较text的部分吻合来获得控件。以下面代码为例子:
~~~
addNote = new UiObject(new UiSelector().textMatches("^Add"));
assertEquals(addNote.getText(),"Add note");
~~~
正常来说这个正则表达式是没有问题的,它的意思就是想要“获取以Add开头的text的控件,至于Add字串口面是什么值,没有必要去管它”。但是按照我们上面的官方描述,这样子是不行的,你必须要把正则表达式补充完整以使得正而表达式和控件的text完全进行匹配,至于你用什么通配符或者字串就完全按照正则表达式的语法了。
注意这个限制在UISlector使用所有的正则表达式相关的方法中都有效哦。
## 3 通过控件的ClassName定位
通过这种方法定位控件存在的一个问题是很容易发生重复,所以一般都是先用这种方法去narrow down目标控件,然后再去添加其他如text判断等条件进行控件定位。
### 3.1 UISelector.className方法
~~~
addNote = new UiObject(new UiSelector().className("android.widget.TextView").text("Add note"));
assertEquals(addNote.getText(),"Add note");
~~~
实例中首先通过ClassName找到所有的TextView控件,然后再在这些TextView控件查找text是”Add note“的控件。
### 3.2 UISelector.classNameMatches方法
~~~
addNote = new UiObject(new UiSelector().classNameMatches(".*TextView$"));
assertEquals(addNote.getText(),"Add note");
~~~
通过正则表达式判断className是否和预期的一致,注意正则表达式的限制和章节2.4描述的一致。
## 4. 通过伪xpath方法定位
UISelector类提供了一些方法根据控件在界面的XML布局中的层级关系来进行定位,但是UIAutomator又没有真正的提供类似Appium的findElementWithXpath相关的方法,所以这里我就称之为伪xpath方法。
这个章节使用到的不再是NotesList那个activity里面的menu options,而是NoteEditor这个activity里面的Menu options,里面不止有一个Menu entry。
### 4.1 通过UiSelector.fromParent或UiObject.getFromParent方法
这种方法我觉得最有用的情况是测试代码当前在操作的是同一层级的一组控件中的某一个控件,转而又需要操作同一层级的另外一个控件的时候。下面的实例就是通过save控件的父控件找到其同一层级的兄弟控件delete。这里分别列出了通过UiObject.getFromParent方法和UiSelector.fromParent方法的实例,事实上他们的功能是一样的。
UiObject.getFromPatrent方法:
~~~
save = new UiObject(new UiSelector().text("Save"));
assertEquals(save.getText(),"Save");
delete = save.getFromParent(new UiSelector().text("Delete"));
assertEquals(delete.getText(),"Delete");
~~~
UiSelector.fromParent方法(这个例子有点迂回笨拙,但为了演示功能就将就着看吧):
~~~
delete = new UiObject(new UiSelector().text("Save").fromParent(new UiSelector().text("Delete")));
assertEquals(delete.getText(),"Delete");
~~~
### 4.2 通过UiSelector.childSelector或UiObject.getChild方法
这种方法是在已知父控件的时候如何快速的查找该父控件下面的子控件。
UiObject.getChild方法:
~~~
UiObject parentView = new UiObject(new UiSelector().className("android.view.View"));
save = parentView.getChild(new UiSelector().text("Save"));
assertEquals(save.getText(),"Save");
~~~
UiSelector.childSelector方法:
~~~
save = new UiObject(new UiSelector().className("android.view.View").childSelector(new UiSelector().text("Save")));
assertEquals(save.getText(),"Save");
~~~
## 5. 通过控件ID定位
在Android API Level18及其以上的版本增加了一个Android控件的属性ResourceId,所以要注意在使用这种方法之前先确保你的目标测试设备和你的UIAutomoator库jar包使用的都是API Level 18以上的版本。例如我自己使用的就是本地sdk中版本19的库:*D:\Develops\AndroidSDK\platforms\android-19\uiautomator.jar*
### 5.1 UiSelector.resourceId方法
~~~
addNote = new UiObject(new UiSelector().resourceId("android:id/title"));
assertEquals(addNote.getText(),"Add note");
~~~
### 5.2 UiSelector.resourceIdMatches方法
~~~
addNote = new UiObject(new UiSelector().resourceIdMatches(".+id/title"));
assertEquals(addNote.getText(),"Add note");
~~~
注意正则表达式的限制和章节2.4描述的一致
## 6. 通过contentDescription定位
在UiAutomator框架和使用了Uiautomator框架的Appium中,控件的属性contentDescription一直是强调开发人员需要添加进去的,因为
- 有些控件使用其他办法很难或者根本没有办法定位
- 最重要的是给每个控件的contentDescription设计个唯一值让我们可以非常快速的定位控件,让我们足够敏捷!
以下的实例并没有真正跑过的,因为Notepad应用上面的控件是没有contentDescription这个属性的,但是如果我们假设Add note这个控件的contentDescription是“AddNoteMenuDesc”的话,代码的相应写法应该就如下了。
### 6.1 UiSelector.description方法
~~~
addNote = new UiObject(new UiSelector().description("AddNoteMenuDesc));
assertEquals(addNote.getText(),"Add note");
~~~
~~~
6.2 UiSelector.descriptionContains方法
addNote = new UiObject(new UiSelector().descriptionContains("AddNote"));
assertEquals(addNote.getText(),"Add note");
~~~
### 6.3 UiSelector.descriptionStartWith方法
~~~
addNote = new UiObject(new UiSelector().descriptionStartsWith("AddNote"));
assertEquals(addNote.getText(),"Add note");
~~~
### 6.4 UiSelector.descriptionMatches方法
~~~
//addNote = new UiObject(new UiSelector().descriptionMatches("^AddNote.*$"));
//assertEquals(addNote.getText(),"Add note");
~~~
## 7.通过其他方法定位
除了以上比较常用的方法外,UIAutomator还支持其他一些方法,比如根据控件属性是否可点击可聚焦可长按等来缩小要定位的控件的范围,具体使用方法不一一列举,可以查看以下测试验证代码。
~~~
package majcit.com.UIAutomatorDemo;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
public class UISelectorFindElementTest extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
UiDevice device = getUiDevice();
device.pressHome();
// Start Notepad
UiObject appNotes = new UiObject(new UiSelector().text("Notes"));
appNotes.click();
//Sleep 3 seconds till the app get ready
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//Evoke the system menu option
device.pressMenu();
UiObject addNote = new UiObject(new UiSelector().text("Add note"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject (new UiSelector().checked(false).clickable(true));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().className("android.widget.TextView").text("Add note"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().classNameMatches(".*TextView$"));
assertEquals(addNote.getText(),"Add note");
//addNote = new UiObject(new UiSelector().description("AddNoteMenuDesc));
//assertEquals(addNote.getText(),"Add note");
//addNote = new UiObject(new UiSelector().descriptionContains("AddNote"));
//assertEquals(addNote.getText(),"Add note");
//addNote = new UiObject(new UiSelector().descriptionStartsWith("AddNote"));
//assertEquals(addNote.getText(),"Add note");
//addNote = new UiObject(new UiSelector().descriptionMatches("^AddNote.*$"));
//assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().focusable(true).text("Add note"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().focused(false).text("Add note"));
assertEquals(addNote.getText(),"Add note");
//TBD
//addNote = new UiObject(new UiSelector().fromParent(selector))
addNote = new UiObject(new UiSelector().index(0).text("Add note"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().className("android.widget.TextView").enabled(true).instance(0));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().longClickable(false).text("Add note"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().text("Add note"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().textContains("Add"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().textStartsWith("Add"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().textMatches("Add.*"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().resourceId("android:id/title"));
assertEquals(addNote.getText(),"Add note");
addNote = new UiObject(new UiSelector().resourceIdMatches(".+id/title"));
assertEquals(addNote.getText(),"Add note");
//Go to the editor activity, need to cancel menu options first
device.pressMenu();
//Find out the new added note entry
UiScrollable noteList = new UiScrollable( new UiSelector().className("android.widget.ListView"));
//UiScrollable noteList = new UiScrollable( new UiSelector().scrollable(true));
UiObject note = null;
if(noteList.exists()) {
note = noteList.getChildByText(new UiSelector().className("android.widget.TextView"), "Note1", true);
//note = noteList.getChildByText(new UiSelector().text("Note1"), "Note1", true);
}
else {
note = new UiObject(new UiSelector().text("Note1"));
}
assertNotNull(note);
//Go to the NoteEditor activity
note.click();
device.pressMenu();
UiObject save = null;
UiObject delete = null;
save = new UiObject(new UiSelector().text("Save"));
assertEquals(save.getText(),"Save");
delete = save.getFromParent(new UiSelector().text("Delete"));
assertEquals(delete.getText(),"Delete");
delete = new UiObject(new UiSelector().text("Save").fromParent(new UiSelector().text("Delete")));
assertEquals(delete.getText(),"Delete");
save = new UiObject(new UiSelector().className("android.view.View").childSelector(new UiSelector().text("Save")));
assertEquals(save.getText(),"Save");
UiObject parentView = new UiObject(new UiSelector().className("android.view.View"));
save = parentView.getChild(new UiSelector().text("Save"));
assertEquals(save.getText(),"Save");
}
}
~~~
';
为网上流行论点“UIAutomator不能通过中文文本查找控件”正名
最后更新于:2022-04-01 19:55:25
## 1. 问题描述和起因
相信大家学习UIAutomator一开始的时候必然会看过一下这篇文章。
- [Android自动化测试(UiAutomator)简要介绍](http://blog.csdn.net/zhubaitian/article/details/39520069)
因为你在百度输入UIAutomator搜索的时候,该文章是排在第一位的。
但是里面有一段说法说UIAutomator不能支持通过中文文本查找控件,这个说法害人不浅,如果不是自己去实践调查过,必然也会轻易放弃UIAutomator以及使用了它的Appium框架,因为本人现在工作上将要测试到的就是全部中文界面的app。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e28948e6.jpg)
最为害人的还是文章中用红色高亮来标志说明这个问题,虽然文章中有评论指出UIAutomator其实是支持中文搜索控件的,但我相信很多人会像我一样一般技术文章只会看正文的。
## 2. 问题分析
做完我自己亲自去编写以下代码去验证这个问题,以下代码的重点主要是最后几行想通过中文来查找控件(假设该控件已经存在)
~~~
package majcit.com.UIAutomatorDemo;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
public class NotePadTest extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
UiDevice device = getUiDevice();
device.pressHome();
// Start Notepad
UiObject appNotes = new UiObject(new UiSelector().text("Notes"));
appNotes.click();
//Sleep 3 seconds till the app get ready
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//Evoke the system menu option
device.pressMenu();
UiObject addNote = new UiObject(new UiSelector().text("Add note"));
addNote.click();
//Add a new note
UiObject noteContent = new UiObject(new UiSelector().className("android.widget.EditText"));
noteContent.clearTextField();
noteContent.setText("Note 1");
device.pressMenu();
UiObject save = new UiObject(new UiSelector().text("Save"));
save.click();
//Find out the new added note entry
UiScrollable noteList = new UiScrollable( new UiSelector().className("android.widget.ListView"));
//UiScrollable noteList = new UiScrollable( new UiSelector().scrollable(true));
UiObject note = null;
if(noteList.exists()) {
note = noteList.getChildByText(new UiSelector().className("android.widget.TextView"), "Note1", true);
//note = noteList.getChildByText(new UiSelector().text("Note1"), "中文笔记", true);
}
else {
note = new UiObject(new UiSelector().text("中文笔记"));
}
assertThat(note,notNullValue());
note.longClick();
UiObject delete = new UiObject(new UiSelector().text("Delete"));
delete.click();
}
}
~~~
运行的时候也确实碰到了如下的问题:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e28b9722.jpg)
当时看到log说"UiOjbectNotFoundException"的时候第一反应就是以上博客高亮指出的问题,以为确实如作者所说的存在这样的问题。但自己换个角度想,这个UIAutomator是google弄的,不可能对unicode字符支持这么基础的东西都有问题啊,所以几经百度谷歌和询问,可怜网上UIAutomator与此相关的资源有如凤毛麟角,几经艰辛才知道这个跟eclipse项目的Text file encoding选项有关系。现在往回想起,以上图片出现乱码的时候其实不应该先先入为主的认为引用博客的作者的说法是正确的,而应该像一网友“小吉”所说的应该先考虑是否是代码文件的编码的问题。
## 3.解决方案
如下图,把项目默认的Text file encoding从GBK改成UTF-8,重新打包运行
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e28e9e54.jpg)
';
Ant如何打包UIAutomator项目用到的第三方JAR包
最后更新于:2022-04-01 19:55:23
本文章主要描述UIAutomator项目中引用到第三方Jar包的时候,按照正常的打包方式碰到的各种问题,以及最终解决的思路和办法。
## 1. 问题起源
在本人的一个示例项目中引用到了单元测试框架hamcrest的jar包,在项目目录下执行ant build的时候出现以下的问题
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e272fee5.jpg)
源码如下:
~~~
package majcit.com.UIAutomatorDemo;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
public class NotePadTest extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
UiDevice device = getUiDevice();
device.pressHome();
// Start Notepad
UiObject appNotes = new UiObject(new UiSelector().text("Notes"));
appNotes.click();
//Sleep 3 seconds till the app get ready
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//Evoke the system menu option
device.pressMenu();
UiObject addNote = new UiObject(new UiSelector().text("Add note"));
addNote.click();
//Add a new note
UiObject noteContent = new UiObject(new UiSelector().className("android.widget.EditText"));
noteContent.clearTextField();
noteContent.setText("Note 1");
device.pressMenu();
UiObject save = new UiObject(new UiSelector().text("Save"));
save.click();
//Find out the new added note entry
UiScrollable noteList = new UiScrollable( new UiSelector().className("android.widget.ListView"));
//UiScrollable noteList = new UiScrollable( new UiSelector().scrollable(true));
UiObject note = null;
if(noteList.exists()) {
note = noteList.getChildByText(new UiSelector().className("android.widget.TextView"), "Note1", true);
//note = noteList.getChildByText(new UiSelector().text("Note1"), "Note1", true);
}
else {
note = new UiObject(new UiSelector().text("Note1"));
}
assertThat(note,notNullValue());
note.longClick();
UiObject delete = new UiObject(new UiSelector().text("Delete"));
delete.click();
}
}
~~~
## 2. 问题分析解决
### 2.1 编译问题分析
根据上图的错误log,很明显我们在实行ant build的时候ant并没有把需要的第三方jar包加入进去进行编译。
根据上一篇文章《[Android自动化测试(UiAutomator)简要介绍](http://blog.csdn.net/zhubaitian/article/details/39520069)》描述,我们在打包UIAutomator项目时会执行一个命令“android create uitest-project -n
-t -p ” 来在项目顶层目录上生成一个build.xml文件,这个文件就ant用来build我们的UIAutomator项目需要用到的配置描述文件。那么很自然我们就会想到去该文件下看是否有把我们需要的jar包给包含进来。
打开该文件查看时,发觉相当精简,并没有太多的东西可看,但是注意到文件末尾引用了我们Android SDK下面的一个文件“${sdk.dir}/tools/ant/uibuild.xml”:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e2768b32.jpg)
打开该文件,里面尽是build相关的配置,所以问题很有可能出现在这里。
找到编译相关的Section,确实没有看到有指定第三方jar包的classpath:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e278cdf8.jpg)
### 2.2 编译问题解决办法
那么很自然,我们应该在这里指定我们第三方jar包的classpath,以便ant在build的时候知道从哪里拿到我们的第三方包。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e27b1c3a.jpg)
我这里的例子是把我项目顶层目录下的“libs”文件夹包含的jar包都引用进来,该目录就是我存放第三方jar包的位置。
运行“ant build”,成功!
### 2.3 运行问题分析
build完成后,满心欢喜的把编译好的jar包push到安卓机器上运行,前期运行的没有问题,但一到调用到第三方Jar包相关API的时候Exception就出来了![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e27dada6.jpg)
编译没有问题,运行时出现问题,那么很有可能就是刚才解决编译问题的时候只是确保项目在编译的时候能找到第三方jar包,但是并没有在编译后把相应的jar包一并打包到目标jar包里面去。
经过一番google,相信还是build配置的问题,返回”${sdk.dir}/tools/ant/uibuild.xml“, 发现确实打包section没有看到第三方jar包相应的信息:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e280cd74.jpg)
### 2.4 运行问题解决办法
根据google提示,最终修改成如下,问题最终解决!
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-08-12_57ad6e285dbc1.jpg)
';
UIAutomator创建一个Note的实例
最后更新于:2022-04-01 19:55:20
紧接之前的创建一个Note的Appium和Robotium的实例,这里给出实现同样功能的UIAutomator的实例如下:
~~~
package majcit.com.UIAutomatorDemo;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiScrollable;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;
public class NotePadTest extends UiAutomatorTestCase {
public void testDemo() throws UiObjectNotFoundException {
UiDevice device = getUiDevice();
device.pressHome();
// Start Notepad
UiObject appNotes = new UiObject(new UiSelector().text("Notes"));
appNotes.click();
//Sleep 3 seconds till the app get ready
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//Evoke the system menu option
device.pressMenu();
UiObject addNote = new UiObject(new UiSelector().text("Add note"));
addNote.click();
//Add a new note
UiObject noteContent = new UiObject(new UiSelector().className("android.widget.EditText"));
noteContent.clearTextField();
noteContent.setText("Note 1");
device.pressMenu();
UiObject save = new UiObject(new UiSelector().text("Save"));
save.click();
//Find out the new added note entry
UiScrollable noteList = new UiScrollable( new UiSelector().className("android.widget.ListView"));
//UiScrollable noteList = new UiScrollable( new UiSelector().scrollable(true));
UiObject note = null;
if(noteList.exists()) {
note = noteList.getChildByText(new UiSelector().className("android.widget.TextView"), "Note1", true);
//note = noteList.getChildByText(new UiSelector().text("Note1"), "Note1", true);
}
else {
note = new UiObject(new UiSelector().text("Note1"));
}
//assertThat(note,notNullValue());
note.longClick();
UiObject delete = new UiObject(new UiSelector().text("Delete"));
delete.click();
}
}
~~~
';
前言
最后更新于:2022-04-01 19:55:18
> 原文出处:[UiAutomator从入门到原理](http://blog.csdn.net/column/details/uiautomatorpriciple.html)
作者:[朱佰添](http://blog.csdn.net/zhubaitian)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# UiAutomator从入门到原理
> 将自己从零基础接触UiAutomator到通过源代码研读学习UiAutomator整套框架的过程记录下来分享给大家
';