本篇文章翻译自Wait for it...a deep dive into Espresso's Idling Resources
开发者面临的挑战之一,是在编写UI测试时需要等待异步计算或I/O操作完成。本篇文章中,我将描述我是如何使用Espresso测试框架来解决这个问题和学到的一些技巧。假设你已经熟悉Espresso,所以,我不会描述它背后的哲学,将重点介绍如何解决上述问题。
Idling Resource
Espresso引入了Idling Resource的概念,它是一个简单的接口:
它代表了被测应用程序的资源,这个资源在测试执行时可以在后台异步工作。
接口定义了三个方法:
- getName():必须返回代表idling resource的非空字符串;
- isIdleNow():返回当前idlingresource的idle状态。如果返回true,onTransitionToIdle()上注册的ResourceCallback必须必须在之前已经调用;
- registerIdleTransitionCallback(IdlingResource.ResourceCallback callback):通常此方法用于存储对回调的引用来通知idle状态的变化。
举个例子,一个idling resource的实现,作用是在WebView中等待页面完全加载:
public class WebViewIdlingResource extends WebChromeClient implements IdlingResource {
private static final int FINISHED = 100;
private WebView webView;
private ResourceCallback callback;
private WebViewIdlingResource(WebView webView) {
this.webView = checkNotNull(webView,
String.format("Trying to instantiate a \'%s\' with a null WebView", getName())));
// Shall we save the original client? Atm it's not used though.
this.webView.setWebChromeClient(this);
}
@Override public void onProgressChanged(WebView view, int newProgress) {
if (newProgress == FINISHED && view.getTitle() != null && callback != null) {
callback.onTransitionToIdle();
}
}
@Override public void onReceivedTitle(WebView view, String title) {
if (webView.getProgress() == FINISHED && callback != null) {
callback.onTransitionToIdle();
}
}
@Override public String getName() {
return "WebView idling resource";
}
@Override public boolean isIdleNow() {
// The webView hasn't been injected yet, so we're idling
if (webView == null) return true;
return webView.getProgress() == FINISHED && webView.getTitle() != null;
}
@Override public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.callback = resourceCallback;
}
}
创建完自定义的idling resource后,需要通过调用Espresso.registerIdlingResource(webViewIdlingResource)与Espresso注册。
注册绑定到Activity实例的组件
有时需要等待的组件是与activity实例绑定的,例如,在webview中完全加载的页面:在这种情况下,事情会变得棘手。为了理解,让我们回到getName()方法,仔细看看文档上面说的:
它用来日志记录和幂等的注册:
下面是来自百度百科对幂等的定义:
幂等是一个数学与计算机学概念,常见于抽象代数中。在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
意思是说,在Espresso.registerIdlingResource(webViewIdlingResource) 第一次调用后,随后所有使用相同名字对idling resource的调用不会产生任何影响(Espresso只是打印警告日志)。通常来说,这没什么大不了的,但是如果idling resource依赖当前上下文(当前的activity),就变成了个问题。拿WebViewIdlingResource来说,为了完成它的工作,它需要当前activity内WebView的引用。一个不假思索的用法可能是这样:
public class WebsiteActivityTestBase extends ActivityInstrumentationTestCase2<WebsiteActivity> {
public WebsiteActivityTestBase() {
super(WebsiteActivity.class);
}
@Override public void setUp() throws Exception {
getActivity();
Espresso.registerIdlingResource(new WebViewIdlingResource(getActivity().findViewById(R.id.webview)));
}
public void testSomething() {}
public void testSomethingElse() {}
}
由于幂等原理,Espresso只会记住第一个注册的idling resource,至少两个原因说明这种用法是不好的:
- 所有的测试用例都依赖idling resource,但是它对webview的引用是无效的,因为已经创建了新的activity;
- 第一个activity的上下文已经泄漏了;
为了解决这个问题,我们可以引入与activity生命周期绑定的idling resource,它将在合适的时候注入并清除对组件的引用。
interface ActivityLifecycleIdlingResource<T> extends IdlingResource {
void inject(T activityComponent);
void clear();
}
现在让我们修改类,实现这个接口:
public class WebViewIdlingResource extends WebChromeClient implements ActivityLifecycleIdlingResource<WebView> {
private WebView webView;
// Same code as above
@Override public void inject(WebView webView) {
this.webView = checkNotNull(webView,
String.format("Trying to instantiate a \'%s\' with a null WebView", getName())));
// Save the original client if needed.
this.webView.setWebChromeClient(this);
}
@Override public void clear() {
// Free up the reference to the webView
webView = null;
}
}
最后一步是找个合适的地方来注入和清除webview的引用,我们可以实现*ActivityLifecycleCallback *接口:
class WebViewInjector implements ActivityLifecycleCallback {
private final WebViewIdlingResource webViewIdlingResource;
WebViewInjector(WebViewIdlingResource webViewIdlingResource) {
this.webViewIdlingResource = webViewIdlingResource;
}
@Override public void onActivityLifecycleChanged(Activity activity, Stage stage) {
ComponentName websiteActivityComponentName =
new ComponentName(activity, WebsiteActivity.class.getName());
if (!activity.getComponentName().equals(websiteActivityComponentName)) return;
switch (stage) {
case CREATED:
// We need to wait for the activity to be created before getting a reference
// to the webview
Fragment webViewFragment =
activity.getFragmentManager().findFragmentByTag(WebsiteScreen.TAG);
webViewIdlingResource.injectWebView((WebView) webViewFragment.getView());
break;
case STOPPED:
// Clean up reference
if (activity.isFinishing()) webViewIdlingResource.clearWebView();
break;
default: // NOP
}
}
}
上述方法看起来不是特别简洁,还有许多细节需要考虑,但确实是我们想要的。
注册绑定到应用程序实例的组件
如果你一定要等待绑定到应用程序对象的组件,事情会变得容易些,因为他是在应用程序的整个生命周期内,通常你不必关心注册/取消注册组件。如今,应用程序90%的功能都依赖API获取数据和需要处理异步I/O。Android系统中有许多方法可以实现,每个都有优缺点,但无论如何,都需要等待API调用返回。如果使用* AsyncTask,Espresso是支持的,但是也有很多理由不使用Android内置组件,而改用Java线程池。在这种情况下,你需要自定义一个idling resource来检查应用程序的执行器是否处于idle状态。查看Espresso源码,稍微重构下 AsyncTaskPoolMonitor类(Espresso使用它来检查 AsyncTask*线程池执行器上是否还有运行的任务),可以实现通用的 ThreadPoolIdlingResource。
总结
为了恰当的使用idling resource,有一些边界情况需要记住。我描述了当前最终实现的解决方案,另一个可能更不容易出错的解决方案是有个Espresso.unregisterIdlingResource(myIdlingResource)API(已经有添加它的功能请求)。至于注册idling resource,我在callApplicationOnCreate(app)方法内自定义了一个InstrumentationTestRunner来实现它,这种方式确保当测试创建时,注册只执行一次。