1

I'm writing an instrumented Android test. When I click the button sample_btn, a Task and a DummyTask start. When the Task ends, the EditText sample_text becomes visible, and I can type some text there.

I was surprised when I noticed the following fact. onView(withId(R.id.sample_btn)).perform(click()); waits for the Task and the DummyTask to complete. Why?

MainActivity

package com.sample;

import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;

import java.lang.ref.WeakReference;

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            findViewById(R.id.sample_btn).setOnClickListener(this);

        }

        @Override
        public void onClick(final View v) {
            switch (v.getId()) {
            case R.id.sample_btn:
                Log.d("UnderstandIsDisplayed", "Before sample_btn click IN CODE");
                new Task(this).execute();
                new DummyTask().execute();
                Log.d("UnderstandIsDisplayed", "After sample_btn click IN CODE");
                break;
            }
        }

        private static class DummyTask extends AsyncTask<Void, Void, Void> {

            @Override
            protected Void doInBackground(final Void... params) {
                try {
                    Log.d("UnderstandIsDisplayed", "DummyTask doInBackground");
                    Thread.sleep(8000);
                    Log.d("UnderstandIsDisplayed", "DummyTask doInBackground done");
                } catch (InterruptedException e) {
                }
                return null;
            }
        }

        private static class Task extends AsyncTask<Void, Void, Void> {

            final WeakReference<MainActivity> mActivityWeakReference;

            Task(MainActivity activity) {
                mActivityWeakReference = new WeakReference<>(activity);
            }

            @Override
            protected Void doInBackground(final Void... params) {
                try {
                    Log.d("UnderstandIsDisplayed", "doInBackground");
                    Thread.sleep(4000);
                    Log.d("UnderstandIsDisplayed", "doInBackground done");
                } catch (InterruptedException e) {
                }
                return null;
            }

            @Override
            protected void onPostExecute(final Void aVoid) {
                super.onPostExecute(aVoid);
                MainActivity mainActivity = mActivityWeakReference.get();
                if (mainActivity != null) {
                    mainActivity.findViewById(R.id.sample_text).setVisibility(View.VISIBLE);
                    Log.d("UnderstandIsDisplayed", "sample_text VISIBLE");
                }
            }
        }

    }

MainActivityTest

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {

    private MainActivity mMainActivity;

    public MainActivityTest() {
        super(MainActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mMainActivity = getActivity();

    }

    public void testFoo() {
        Log.d("UnderstandIsDisplayed", "Before sample_btn click");
        onView(withId(R.id.sample_btn)).perform(click());
        Log.d("UnderstandIsDisplayed", "After sample_btn click");
        // TODO: Wait for sample_text to become visible
        onView(withId(R.id.sample_text)).perform(typeText("sample_text"));

    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:id="@+id/container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#336633"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.sample.MainActivity">

    <Button
        android:id="@+id/sample_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="sample_btn"/>

    <EditText
        android:id="@+id/sample_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"/>

</LinearLayout>

build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "24.0.1"

    defaultConfig {
        applicationId "com.sample"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.0.1'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
}

This is what I see when running the app itself:

09-07 12:09:02.623 22356-22356/com.sample D/UnderstandIsDisplayed: Before sample_btn click IN CODE
09-07 12:09:02.629 22356-22660/com.sample D/UnderstandIsDisplayed: doInBackground 1562
09-07 12:09:02.631 22356-22356/com.sample D/UnderstandIsDisplayed: After sample_btn click IN CODE
09-07 12:09:06.670 22356-22660/com.sample D/UnderstandIsDisplayed: doInBackground done
09-07 12:09:06.672 22356-22356/com.sample D/UnderstandIsDisplayed: sample_text VISIBLE
09-07 12:09:06.680 22356-22771/com.sample D/UnderstandIsDisplayed: DummyTask doInBackground 1563
09-07 12:09:14.721 22356-22771/com.sample D/UnderstandIsDisplayed: DummyTask doInBackground done

This is what I see when running testFoo:

09-07 12:10:24.357 24892-24911/com.sample D/UnderstandIsDisplayed: testFoo = 1555
09-07 12:10:24.357 24892-24911/com.sample D/UnderstandIsDisplayed: Before sample_btn click
09-07 12:10:24.429 24892-24892/com.sample D/UnderstandIsDisplayed: Before sample_btn click IN CODE
09-07 12:10:24.431 24892-24954/com.sample D/UnderstandIsDisplayed: doInBackground 1563
09-07 12:10:24.431 24892-24892/com.sample D/UnderstandIsDisplayed: After sample_btn click IN CODE
09-07 12:10:28.465 24892-24954/com.sample D/UnderstandIsDisplayed: doInBackground done
09-07 12:10:28.469 24892-24892/com.sample D/UnderstandIsDisplayed: sample_text VISIBLE
09-07 12:10:28.472 24892-24936/com.sample D/UnderstandIsDisplayed: DummyTask doInBackground 1558
09-07 12:10:36.483 24892-24936/com.sample D/UnderstandIsDisplayed: DummyTask doInBackground done
09-07 12:10:36.702 24892-24911/com.sample D/UnderstandIsDisplayed: After sample_btn click
piotrek1543
  • 19,130
  • 7
  • 81
  • 94
Maksim Dmitriev
  • 5,985
  • 12
  • 73
  • 138

2 Answers2

1

Using Espresso you are allowed to operate only inside your app under test context. It means that Espresso for perform any action needs to operate on main thread of app. It checks when UI thread is idle() and if not it waits until UI thread would be idle again.

Check: http://dev.jimdo.com/2014/05/09/wait-for-it-a-deep-dive-into-espresso-s-idling-resources/

In some cases, it produces Espressso IdlingResources errors like AppNotIdleState: https://developer.android.com/reference/android/support/test/espresso/AppNotIdleException.html

Example 1#: https://github.com/81813780/AVLoadingIndicatorView/issues/16

Example 2#: Espresso test fails after successful click and blocking for 60 seconds

To deal with this problem you can create your own IdlingResource method to say Espresso when UI thread is idle for him.

Check: https://google.github.io/android-testing-support-library/docs/espresso/idling-resource/index.html

Exmaple: http://blog.sqisland.com/2015/04/espresso-custom-idling-resource.html

If it won't work try to use along with Espresso another Google test framework called uiatomator, which may help you to deal with this problem.

In my case, writing my own IdlingResources hadn't work, but mixing Espresso with Robotium (doesn't wait for idle UI thread) and uiautomator had done its job.

Hope it will help

Community
  • 1
  • 1
piotrek1543
  • 19,130
  • 7
  • 81
  • 94
1

The AsyncTask handling is baked into espresso framework in UiControllerImpl.loopMainThreadUntilIdle(). For the most cases, it is pretty convenient that espresso handles some of the background work out of the box. Unfortunately, there is no available solution to turn off any of the provided handlers (UI thread synchronization, async task). Which makes testing progress indicators or testing interruption pretty complicated.

There is an open issue in espresso bug tracker, but it hasn't got much attention.

Be_Negative
  • 4,892
  • 1
  • 30
  • 33
  • AsyncTasks are pretty rare these days? – Maksim Dmitriev Sep 08 '16 at 08:32
  • Sorry, what I meant is - with this massive push from the community towards reactive programming and with a great selection of open source libraries that are available for networking and background work, AsyncTasks are dying off. I understand that there might be legacy code bases, that have to be maintained and sometimes there is no other option available. And there might be other legit reasons why people opt to use AsyncTasks. But this is all off-topic, and I will remove that line as it has nothing to do with the question asked. – Be_Negative Sep 08 '16 at 23:49