Is it possible to stop idling for an AsyncTask to complete?

Viewed 182

I am trying to set up a test for my project - to test that a progress bar is displayed when my app performs a server request.

The code under test uses an AsyncTask to perform the network call.

I have created a blocking server (MockWebServer) to catch and hold the network call - it receives request but doesn't provide a response until i call ".release()". This allows me to verify before the server response occurs.

My logic flows like this:

// Mock server will catch the next network request
BlockingServer blockingServer = createBlockingServer();

// onResume() activity performs network request and shows Progress Spinner
activityTestRule.launchActivity(null);

// onView() waits on UiController.loopUntilIdle() <- Fails here due to timeout.
onView(withId(progressBar)).check(matches(isDisplayed()));

// Tells the server to respond to the network request
blockingServer.release();

onView(withId(progressBar)).check(matches(not(isDisplayed()))); 

My problem is that because the Code Under Test uses AsyncTask for the server request, Espresso naturally blocks on the verify call (onView()) in order to wait for the AsyncTask to complete before verifying.

What I need is to temporarily stop Espresso idling while waiting for AsyncTask in order to perform the verify while the server is blocking the app logic flow.

(Changing the Code Under Test is not an option)

Can someone help?

1 Answers

So... this is the answer I've arrived at and some working out behind it:

Espresso (specifically calls to onView(), onData(), injectEvent and Actions) uses UiControllerImpl.loopMainThreadUntilIdle() to wait until all "idle-causing" signals are false. It loops over AsyncTask, CompatAsyncTask and something called dynamicIdle to all be idle.

When this method returns the main flow continues.

loopMainThreadUtilIdle() checks an IdleNotifier to check the idle state of each of those three elements. Obviously if you want to stop espresso waiting for AsyncTask the asyncIdle is of particular interest to you.

The IdleNotifier classes are fed into UiControllerImpl at it's construction - this takes place via dagger so you'll need to look at DaggerBaseLayerComponent which uses Providers to grab the construction arguments and pass them into the UiControllerProvider to construct it.

Everything in all of these classes is locked down very tightly. Method and class visibility is usually protected or package-private and final.

The only way I found was to create my own Espresso.java class (onView() and onData()) which used custom DaggerBaseLayerComponent allowing me to use either: My own Providers or My own UiController.

I found however this doesn't solve the whole problem. There is one more mechanism that needs to be coded around - When you're starting activities they use a waitForIdleSync in the Instrumentation class. Usually this is the Runner which is provided in your gradle file. I created my own AndroidJUnitRunner and provided this in gradle to allow me to return from waitForIdleSync on command.

And finally, in startActivitySync in the Instrumentation base class, it uses an array of ActivityWaiter objects to hold up your launchIntent() calls. I couldn't think of a reasonable way of avoiding this so I cheated and created this method in my Runner:

    public void clearActivityWaitQueue() {
        Object mSync = Whitebox.getInternalState(this, "mSync");
        List mWaitingActivities = Whitebox.getInternalState(this, "mWaitingActivities");
        if (mSync != null && mWaitingActivities != null) {
            mWaitingActivities.clear();
            synchronized (mSync) {
                mSync.notifyAll();
            }
        }
    }

It uses PowerMock to give me the convenience Whitebox methods to set internal state of Instrumentation:

// Used to give access to Whitebox
androidTestImplementation 'org.powermock:powermock-reflect:1.6.5'

And that's it! Easy right?

(Please tell me it's easier than this and how!!)

Related