深入研究Espresso的Idling Resource

本篇文章翻译自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来实现它,这种方式确保当测试创建时,注册只执行一次。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容