Android Instrumentation Testing - UI Thread Issues

22,863

Solution 1

Those instrumentation tests run inside their own app. This also means, they run in their own thread.

You must think of your instrumentation as something you install alongside your actual app, so your possible interactions are 'limited'.

You need to call all view methods from the UIThread / main thread of the application, so calling activity.updateDetails(workOrder); from your instrumentation thread is not the application main thread. This is why the exception is thrown.

You can just run the code you need to test on your main thread like you would do if you were calling it inside your app from a different thread by using

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

With this running your test should work.

The illegal state exception you are receiving seems to be because of your interaction with the rule. The documentation states

Note that instrumentation methods may not be used when this annotation is present.

If you start / get your activity in @Before it should also work.

Solution 2

You can run portion of your test on the main UI thread with the help of 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")));
}

In most cases it is simpler to annotate the test method with UiThreadTest, however, it may incur other errors such as java.lang.IllegalStateException: Method cannot be called on the main application thread (on: main).

FYR, here is a quote from UiThreadTest's Javadoc:

Note, due to current JUnit limitation, methods annotated with Before and After will also be executed on the UI Thread. Consider using runOnUiThread(Runnable) if this is an issue.

Please note UiThreadTest (package android.support.test.annotation) mentioned above is different from (UiThreadTest (package android.test)).

Solution 3

The accepted answer is now deprecated

The easiest way to achieve this is simply using 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
        }

Solution 4

With the androidx test runner a new class was added UiThreadStatement that gives a runOnUiThread method for this.

        UiThreadStatement.runOnUiThread {
           // call activity here 
        }

Solution 5

The accepted answer describes what is going on perfectly.

As an addition, in case someone is curious why Espresso's methods that touch the UI e.g. perform(ViewActions ...) don't need to do the same, it is simply because they end up doing it later for us.

If you follow perform(ViewActions ...) you will find it ends up doing the following (in android.support.test.espresso.ViewInteraction):

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

That mainThreadExecutor is itself annotated with @MainThread.

In other words, Espresso also needs to play by the same rules described by David on the accepted answer.

Share:
22,863
Khalos
Author by

Khalos

Updated on December 14, 2020

Comments

  • Khalos
    Khalos over 3 years

    I am trying to write an Instrumentation Test for my Android app.

    I'm running into some weird threading issues and I can't seem to find a solution.

    My Original Test:

    @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")));
        }
    }
    

    This resulted in an

    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

    ...

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

    The only thing the updateDetails() method does is some setText() calls.

    After researching a bit, it seemed like adding a UiThreadTestRule and android.support.test.annotation.UiThreadTest annotation to my test would fix the problem.

    @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: Method cannot be called on the main application thread (on: main)

    (Note: All of the methods in this stack trace are not my code)

    It seems to be giving me mixed results... If it needs to be run on the original thread that created the views but can't run on the main thread, what thread should it be run on?

    I'd really appreciate any help or suggestions!