You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决每秒重绘视图引发的Espresso AppNotIdleException问题?

解决Espresso因持续重绘导致的AppNotIdleException问题

首先得纠正你对noActivity()的误解:这个方法根本不是这么用的!它是ActivityScenario的API,用来获取一个无关联Activity的场景实例,和跳过空闲等待完全没关系,你之前的写法本身就有问题,这也是测试跳过点击停止按钮直接执行tearDown()的原因。

回到核心问题:Espresso默认会等待主线程空闲+所有注册的IdleResource空闲才会执行下一步操作,而你的仪表每秒重绘一次,相当于主线程一直在处理动画/绘制任务,Espresso会一直认为应用处于忙碌状态,最终超时抛出AppNotIdleException。下面给你几个可行的解决方案,按从简单到精准的顺序排列:

方案1:强制跳过空闲等待(快速解决单个测试)

如果你只是想让测试直接点击停止按钮,不管应用是否空闲,可以自定义一个ViewAction,不调用Espresso默认的等待空闲逻辑:

Kotlin代码

@Test
fun testGaugeStartStop() {
    // 点击启动按钮
    onView(withId(R.id.start_button)).perform(click())
    
    // 自定义点击动作,不等待主线程空闲
    onView(withId(R.id.stop_button)).perform(object : ViewAction {
        override fun getConstraints() = isDisplayed()
        override fun getDescription() = "Click stop button without waiting for idle"
        override fun perform(uiController: UiController, view: View) {
            // 直接触发点击,跳过loopMainThreadUntilIdle()
            view.performClick()
        }
    })
}

Java代码

@Test
public void testGaugeStartStop() {
    onView(withId(R.id.start_button)).perform(click());
    
    onView(withId(R.id.stop_button)).perform(new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isDisplayed();
        }

        @Override
        public String getDescription() {
            return "Click stop button without waiting for idle";
        }

        @Override
        public void perform(UiController uiController, View view) {
            view.performClick();
        }
    });
}

方案2:注册自定义IdleResource(精准控制空闲条件)

如果想让Espresso在停止按钮可见时就判定应用空闲(符合你业务逻辑的等待条件),可以编写一个自定义IdleResource,让它检查停止按钮的可见状态:

自定义IdleResource(Kotlin)

class StopButtonVisibleIdlingResource(private val activity: Activity) : IdlingResource {
    private var callback: ResourceCallback? = null

    override fun getName() = "StopButtonVisibleIdlingResource"

    override fun isIdleNow(): Boolean {
        val stopButton = activity.findViewById<Button>(R.id.stop_button)
        val isButtonVisible = stopButton != null && stopButton.isShown
        
        // 如果按钮可见,通知Espresso进入空闲状态
        if (isButtonVisible) {
            callback?.onTransitionToIdle()
        }
        return isButtonVisible
    }

    override fun registerIdleTransitionCallback(callback: ResourceCallback?) {
        this.callback = callback
    }
}

测试中使用(Kotlin)

@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)

@Test
fun testGaugeStartStop() {
    lateinit var idlingResource: StopButtonVisibleIdlingResource
    
    activityRule.scenario.onActivity { activity ->
        // 初始化并注册空闲资源
        idlingResource = StopButtonVisibleIdlingResource(activity)
        IdlingRegistry.getInstance().register(idlingResource)
    }
    
    // 点击启动按钮
    onView(withId(R.id.start_button)).perform(click())
    
    // Espresso会自动等待直到空闲资源返回true(停止按钮可见),再执行点击
    onView(withId(R.id.stop_button)).perform(click())
    
    // 测试结束后注销资源,避免影响其他测试
    IdlingRegistry.getInstance().unregister(idlingResource)
}

Java版本的自定义IdleResource和测试代码

public class StopButtonVisibleIdlingResource implements IdlingResource {
    private final Activity activity;
    private ResourceCallback callback;

    public StopButtonVisibleIdlingResource(Activity activity) {
        this.activity = activity;
    }

    @Override
    public String getName() {
        return "StopButtonVisibleIdlingResource";
    }

    @Override
    public boolean isIdleNow() {
        Button stopButton = activity.findViewById(R.id.stop_button);
        boolean isButtonVisible = stopButton != null && stopButton.isShown();
        
        if (isButtonVisible) {
            if (callback != null) {
                callback.onTransitionToIdle();
            }
        }
        return isButtonVisible;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
        this.callback = callback;
    }
}

测试代码:

@Rule
public ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);

@Test
public void testGaugeStartStop() {
    StopButtonVisibleIdlingResource idlingResource = null;
    
    activityRule.getScenario().onActivity(activity -> {
        idlingResource = new StopButtonVisibleIdlingResource(activity);
        IdlingRegistry.getInstance().register(idlingResource);
    });
    
    onView(withId(R.id.start_button)).perform(click());
    onView(withId(R.id.stop_button)).perform(click());
    
    if (idlingResource != null) {
        IdlingRegistry.getInstance().unregister(idlingResource);
    }
}

方案3:全局禁用自动空闲等待(不推荐,适合特殊场景)

如果你有很多测试都需要跳过空闲等待,可以在测试类的@Before方法中注册一个始终返回空闲的IdleResource,让Espresso认为应用一直处于空闲状态:

Kotlin代码

@Before
fun setup() {
    val alwaysIdleResource = object : IdlingResource {
        override fun getName() = "AlwaysIdleResource"
        override fun isIdleNow() = true
        override fun registerIdleTransitionCallback(callback: ResourceCallback?) {}
    }
    IdlingRegistry.getInstance().register(alwaysIdleResource)
}

@After
fun teardown() {
    IdlingRegistry.getInstance().clear()
}

这种方法不推荐,因为它会让Espresso失去等待异步任务的能力,可能导致其他测试出现不稳定的情况,只适合所有测试都不需要等待空闲的场景。


内容的提问来源于stack exchange,提问作者elock

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:59:14