Android仪器化测试 - UI线程问题

36

我想为我的Android应用程序编写一个 Instrumentation 测试。

我遇到了一些奇怪的线程问题,似乎找不到解决方法。

我的原始测试:

@RunWith(AndroidJUnit4.class)
public class WorkOrderDetailsTest {

    @Rule
    public ActivityTestRule<WorkOrderDetails> activityRule = new ActivityTestRule<>(WorkOrderDetails.class);

    @Test
    public void loadWorkOrder_displaysCorrectly() throws Exception {
        final WorkOrderDetails activity = activityRule.getActivity();

        WorkOrder workOrder = new WorkOrder();
        activity.updateDetails(workOrder);

        //Verify customer info is displayed
        onView(withId(R.id.customer_name))
                .check(matches(withText("John Smith")));
    }
}

这导致了一个

android.view.ViewRootImpl$CalledFromWrongThreadException: 只有创建视图层次结构的原始线程才能触摸其视图。

...

com.kwtree.kwtree.workorder.WorkOrderDetails.updateDetails(WorkOrderDetails.java:155)

updateDetails() 方法所做的唯一事情就是一些 setText() 调用。

经过一番研究,似乎将 UiThreadTestRuleandroid.support.test.annotation.UiThreadTest 注释添加到我的测试中可以解决问题。

@UiThreadTest:

@RunWith(AndroidJUnit4.class)
public class WorkOrderDetailsTest {

    //Note: This is new
    @Rule
    public UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();

    @Rule
    public ActivityTestRule<WorkOrderDetails> activityRule = new ActivityTestRule<>(WorkOrderDetails.class);

    @Test
    @UiThreadTest //Note: This is new
    public void loadWorkOrder_displaysCorrectly() throws Exception {
        final WorkOrderDetails activity = activityRule.getActivity();

        WorkOrder workOrder = new WorkOrder();
        activity.updateDetails(workOrder);

        //Verify customer info is displayed
        onView(withId(R.id.customer_name))
                .check(matches(withText("John Smith")));
    }
}

java.lang.IllegalStateException: 该方法无法在主应用程序线程上调用(位于:main)

(注意:此堆栈跟踪中的所有方法都不是我的代码)

看起来它给了我混合的结果......如果它需要在创建视图的原始线程上运行,但无法在主线程上运行,那么应该在哪个线程上运行?

我真的很感激任何帮助或建议!

5个回答

37

这些仪器测试在其自己的应用内运行。这也意味着它们在自己的线程中运行。

您必须将您的仪器测试视为与您的实际应用程序一起安装的内容,因此您的可能相互作用是“有限的”。

您需要从UIThread /主线程中调用所有视图方法,因此从您的仪器测试线程调用activity.updateDetails(workOrder);不是应用程序的主线程。这就是为什么会抛出异常的原因。

您可以像在从不同线程调用应用程序内部的代码时那样,在主线程上运行您需要测试的代码,方法是使用

activity.runOnUiThread(new Runnable() {
    public void run() {
        activity.updateDetails(workOrder);
    }
}

按照这种方法运行您的测试应该可以工作。

您收到的非法状态异常似乎是由于与规则的交互引起的。 文档 指出:

请注意,当此注释存在时,不得使用仪器方法。

如果您在 @Before 中启动 / 获取您的活动,它也应该可以工作。


3
当与 getInstrumentation().waitForIdleSync(); 结合使用时,runOnUiThread 方法可行。很遗憾,@Before 方法无法生效。感谢您的帮助! - Khalos
1
这一点非常重要,特别是当你紧接着一个断言时,否则你可能会出现竞态条件,导致测试结果不一致! - hmac
1
如果可以通过各种方式切换到UI线程,那么为什么Runner不能一开始就利用主线程呢? - stdout

19
你可以通过UiThreadTestRule.runOnUiThread(Runnable)在主线程上运行部分测试:
@Rule
public UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();

@Test
public void loadWorkOrder_displaysCorrectly() throws Exception {
    final WorkOrderDetails activity = activityRule.getActivity();

    uiThreadTestRule.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            WorkOrder workOrder = new WorkOrder();
            activity.updateDetails(workOrder);
        }
    });

    //Verify customer info is displayed
    onView(withId(R.id.customer_name))
            .check(matches(withText("John Smith")));
}

在大多数情况下,使用UiThreadTest注释测试方法更简单,但是可能会产生其他错误,例如java.lang.IllegalStateException: Method cannot be called on the main application thread (on: main)。请注意,上面提到的android.support.test.annotation包中的UiThreadTestandroid.test包中的UiThreadTest不同。此外,以下是UiThreadTest Javadoc中的一句话:“请注意,由于当前JUnit的限制,带有BeforeAfter注释的方法也将在UI线程上执行。如果存在问题,请考虑使用runOnUiThread(Runnable)。”

15

接受的答案现在已经不建议使用

最简单的方法是直接使用UiThreadTest

import android.support.test.annotation.UiThreadTest;

        @Test
        @UiThreadTest
        public void myTest() {
            // Set up conditions for test

            // Call the tested method
            activity.doSomethingWithAView()

            // Verify that the results are correct
        }

1
如果未来的访问者遇到“Android.Test.UIThreadTest已在API 24中弃用:Android 7.0(Nougat)”,请注意这不是相同的注释。此答案使用支持注释。但我无法解决这个问题。 - lucidbrot
这个答案在我的Nougat上有效。 - lucidbrot
1
myTest 中,activity 是从哪里来的?这就是我卡住的地方。 - Phlip

11

使用androidx测试运行器,增加了一个新类 UiThreadStatement,它提供了一个 runOnUiThread 方法。

        UiThreadStatement.runOnUiThread {
           // call activity here 
        }

1

被接受的答案完美地描述了正在发生的事情。

此外,如果有人好奇为什么Espresso的触摸UI的方法(例如perform(ViewActions ...))不需要执行相同的操作,那只是因为它们最终会为我们执行这些操作。

如果你跟随perform(ViewActions ...),你会发现它最终会执行以下操作(在android.support.test.espresso.ViewInteraction中):

private void runSynchronouslyOnUiThread(Runnable action) {
    ...
    mainThreadExecutor.execute(uiTask);
    ...
}

mainThreadExecutor本身也带有@MainThread的注释。

换句话说,Espresso也需要遵守David在被接受的答案中描述的相同规则。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接