(本文基于:'androidx.test.uiautomator:uiautomator:2.2.0')
前言
在Ui功能自动化测试过程中,经常需要短暂停顿后再去查找控件或者长时间的停顿用来模拟App的页面停留。
查找控件过程中的短暂停留非常重要,如果每次都以某一时刻(某一瞬间)去查找控件,而每台手机展示控件的时机是不一样的,这样会造成同一套程序代码有时可以正常工作,有时却根本找不到控件。
长时间停顿也很重要,某些页面需要长时间存活,比如直播App中的直播间页面就需要保持60s的存活,通过编程方式去模拟页面的存活,我们如何用编程方式模拟短暂停顿以及长期停顿的测试需求?使用UI Automator测试框架中提供的等待功能即可。
如果没有等待功能,UI自动化测试一定很惨,程序中的测试用例只能在特定的手机上运行,因为环境不同,所以控件出现的时机则不同(控件出现可能耗时50ms、也可能耗时300ms),查找控件的方法虽然默认自带重试逻辑,但默认的重试逻辑往往不能满足需求,此时你的Ui功能自动化程序因为网络、机型、等等各种环境的不同,造成控件无法找到!!
如何才能做到使用同一套程序在每个手机上都能正确无误的查找到控件呢?这就必须得使用UI Automator测试框架提供的显式等待功能(或者你自己增加重试逻辑也行)。
通过UiDevice对象或UiObject2对象提供的API可以显式的等待……这篇文章将分析UiDevice提供的显式等待功能是如何实现的?总体上,UiDevice的等待功能根据不同的用途,分为两个类别:
1、等待指定的条件满足或者达到停顿的最大时间,插桩测试线程才能继续运行!
2、等待被测试App的主线程空闲后,插桩测试线程继续运行,插桩测试线程的等待是为了减少对被测应用的干扰!(别抢占CPU时间片)(这个仅限于插桩测试与App在同一个进程中)
无论使用哪个等待功能,都会造成插桩测试线程处于停顿的阻塞状态。(底层实现为插桩线程休眠,此时的插桩线程处于TIMED_WAITING状态)
UiDevice类中定义的所有等待方法
UiDevice的等待功能
1、给定条件满足,插桩测试线程才会继续运行,否则插装测试线程将短暂停顿,以固定的时间询问条件是否已经符合要求(实际通过持有的WaitMixin对象的方法实现的该功能)
perfromActionAndWait()方法
wait(SearchCondition,long)方法
wiatForWindowUpdate()方法
2、等待被测试应用处于空闲状态(主线程空闲),插桩测试继续运行,否则插装测试处于停顿中(实际通过UiAutomation的方法实现的该功能)
waitForIdle()
WaitForIdle(long)
条件满足后,插桩测试线程继续运行的功能分析
1、wait()方法
接受一个SearchCondition对象,它表示停顿条件(搜索条件),该条件决定插桩测试线程能否继续运行!另一个参数为long类型的参数,表示插桩测试线程停顿的最长时间,超过该时间,插桩测试线程将继续运行
public <R> R wait(SearchCondition<R> condition, long timeout) {
return mWaitMixin.wait(condition, timeout);
}
wait()方法是我们经常会使用的等待功能,每次调用该方法时,需要使用UI Automator测试框架中的另一个主要类Until,Until类的静态方法可以返回需要的SearchCondition对象。wait()方法本身是范型方法,必须传入的SearchCondition对象的类型参数<R>最后会成为整个wait()方法的返回值类型。
第1个参数为SearchCondition对象,表示插桩测试能否继续运行的条件(在插桩测试线程处于停顿的过程中,每间隔1s,就会调用该SearchCondition对象的apply()方法,如果嫌弃这个停顿时间久,可自定义重试),只有条件的结果为允许时,插桩测试线程才能继续运行;
第2个参数是long类型,表示插桩测试线程停顿的最大时间(以毫秒为单位),当插桩线程达到最大的停顿时间,插桩测试线程会继续运行!
在wait()方法的内部,直接通过调用UiDevice对象持有的WaitMixin对象mWaitMixin的wait()方法(注意:与UiDevice的wait()方法同名)完成实际等待的操作。WaitMixin对象的wait()方法也需要两个参数,一个参数是SearchCondition对象,另一个参数是long类型,这里直接将UiDevice的wait()方法的局部变量condition、局部变量timeout直接透传给WaitMixin对象的wait()方法,当WaitMixin对象的wait()方法执行结束后,它的返回值将成为UiDevice的wait()方法的返回值!(备注:WaitMixin的wait()方法的具体分析将在WaitMixin专题文章中讲解)
2、performActionAndWait()方法
接受3个参数,一个Runnable对象、一个EventCondition对象、一个long型
public <R> R performActionAndWait(Runnable action, EventCondition<R> condition, long timeout) {
AccessibilityEvent event = null;
try {
event = getUiAutomation().executeAndWaitForEvent(
action, new EventForwardingFilter(condition), timeout);
} catch (TimeoutException e) {
// Ignore
}
if (event != null) {
event.recycle();
}
return condition.getResult();
}
这个方法会根据EventCondition对象的结果,执行Runnable对象的run()方法,我们学习一下内部的实现!
第一个参数是Runnable对象,它表示执行的任务
第二个参数是EventCondition对象,它是决定Runnable任务能否执行的条件
第三个参数是long类型,它表示插桩线程停顿的最大时间
首先创建局部变量event,它的类型是AccessibilityEvent,那么它表示什么呢?通过官方文档所述:它表示一个事件
接下来通过得到一个UiAutomation对象,调用它的executeAndWaitForEvent()方法,这个方法来说实现的挺长……将在UiAutomation专栏中直接分析(后续文章)
等待被测应用主线程空闲后继续运行插桩测试的方法分析
1、waitForIdle方法()
无参数
public void waitForIdle() {
Tracer.trace();
getQueryController().waitForIdle();
}
该方法的作用:等待被测试应用处于空闲状态,再让插桩线程继续运行测试,被测试应用是否处于空闲状态,是由被测试应用的主线程的忙碌状态决定的,该方法默认要求插桩测试线程等待10s!在该方法内部通过调用Tracer的静态方法trace()来获得堆栈信息!接着是获得一个QueryController对象,然后调用它的waitForIdle()方法完成实际工作。UiDevice对象持有了一个QueryController对象,我们也终于看到这个对象提供的waitForIdle()功能,接下来就进去看看该QueryController的waitForIdle()方法
2、QueryController中定义的waitForIdle()方法
public void waitForIdle() {
waitForIdle(Configurator.getInstance().getWaitForIdleTimeout());
}
内部直接调用一个重载的waitForIdle方法,我们再进去看看!
3、QueryController中定义的waitForIdle()方法
接受一个超时时间timeout
public void waitForIdle(long timeout) {
try {
UiDevice.getUiAutomation(getInstrumentation())
.waitForIdle(QUIET_TIME_TO_BE_CONSIDERED_IDLE_STATE, timeout);
} catch (TimeoutException e) {
Log.w(LOG_TAG, "Could not detect idle state.");
}
}
你还记得在UiDevice对象、QueryController对象同时持有的是同一个Instrumentation对象吗?这个为了从UiDevce中获取UiAutoation对象,又将QueryController对象持有的Instrumentation对象传递了进去,又调用了UiAutomation的waitForIdle方法!我们马上就看看这个方法的实现,看来这个UiAutomation对象真的是功能很多!
4、位于UiAutomation中的waitForIdle()方法
public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis)
throws TimeoutException {
synchronized (mLock) {
throwIfNotConnectedLocked();
final long startTimeMillis = SystemClock.uptimeMillis();
if (mLastEventTimeMillis <= 0) {
mLastEventTimeMillis = startTimeMillis;
}
while (true) {
final long currentTimeMillis = SystemClock.uptimeMillis();
// Did we get idle state within the global timeout?
final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis;
final long remainingGlobalTimeMillis =
globalTimeoutMillis - elapsedGlobalTimeMillis;
if (remainingGlobalTimeMillis <= 0) {
throw new TimeoutException("No idle state with idle timeout: "
+ idleTimeoutMillis + " within global timeout: "
+ globalTimeoutMillis);
}
// Did we get an idle state within the idle timeout?
final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis;
final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis;
if (remainingIdleTimeMillis <= 0) {
return;
}
try {
mLock.wait(remainingIdleTimeMillis);
} catch (InterruptedException ie) {
/* ignore */
}
}
}
}
留下来分析UiAutomation的一系列方法!
总结
1、插装线程处于停顿时,并没有完全停顿,而是每间隔1秒回调SearchCondition对象的apply()方法,询问条件是否已经满足?(这就是轮询)
2、插装线程停顿结束两种方式,一种是条件满足、另一种是达到最大停顿时间,插装测试就会继续运行了!(如果算上异常的话,线程整个都会结束、还有中断、我的妈妈呀!)