Published on

Android Test For UI Thread Listeners

Authors

The other day I wrote an Android downloading service running in a separate process. The main application process will send command/message to the downloading process asking it to download item. The downloading process is supposed to post back the downloading process and status to the main application process by sending messages. Everything works fine: the application process can send command and downloading process can receive the command and then start downloading the item. Also the application process can also receive the process update messages from the downloading process.

For the sake of recursive testing, namely, I wrote the following test case, following the direction specified by Android SDK documentation. Then run in Eclipse, everything worked fine, and I did not put much effort on it, it got into the source depot.

public class DownloadAgentTest extends ActivityInstrumentationTestCase2<UtActivity> {
    private static final String LOG_TAG = DownloadAgentTest.class.getName();
    public DownloadAgentTest() {
        super("com.example", com.ut.UtActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        mContext = mActivity.getBaseContext();
    }

    private UtActivity mActivity;
    private Context mContext;

    @Test
    public void testDownloadAgent() {
        final CountDownLatch signal = new CountDownLatch(1);

        IDownloadListener listener = new IDownloadListener() {
            @Override
            public void onStart() {
                Log.d(LOG_TAG, "onStart");
            }

            @Override
            public void onProgressUpdate(int progress) {
                Log.d(LOG_TAG, "onProgressUpdate(" + progress + ")");
            }

            @Override
            public void onEnd(int result, String file) {
                Log.d(LOG_TAG, "onEnd(" + result + ", " + file + ")");
                Assert.assertTrue(result == 1);
                Assert.assertTrue(file.contains("/sdcard/"));
                signal.countDown();
            }
        };
        DownloadAgent agent = new DownloadAgent(
            mContext,
            "xp",
            "App_name",
            "http://www.google.com/some.apk",
            listener);
        agent.start();
        try {
            signal.await(100, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Days later, one of my colleagues read that code, run it, but asked: "How do you know functions onProgressUpdate and onEnd are called?". Inspired by his question, I updated the code as:

public class DownloadAgentTest extends ActivityInstrumentationTestCase2<UtActivity> {
    private static final String LOG_TAG = DownloadAgentTest.class.getName();

    boolean onStartTriggered = false;
    boolean onProgressUpdateTriggered = false;
    boolean onEndTriggered = false;

    public DownloadAgentTest() {
        super("com.example", com.ut.UtActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        mContext = mActivity.getBaseContext();
    }

    private UtActivity mActivity;
    private Context mContext;

    @Test
    public void testDownloadAgent() {
        final CountDownLatch signal = new CountDownLatch(1);

        onStartTriggered = false;
        onProgressUpdateTriggered = false;
        onEndTriggered = false;

        IDownloadListener listener = new IDownloadListener() {
            @Override
            public void onStart() {
                Log.d(LOG_TAG, "onStart");
                onStartTriggered = true;
            }

            @Override
            public void onProgressUpdate(int progress) {
                Log.d(LOG_TAG, "onProgressUpdate(" + progress + ")");
                onProgressUpdateTriggered = true;
            }

            @Override
            public void onEnd(int result, String file) {
                onEndTriggered = true;
                Log.d(LOG_TAG, "onEnd(" + result + ", " + file + ")");
                Assert.assertTrue(result == 1);
                Assert.assertTrue(file.contains("/sdcard/"));
                signal.countDown();
            }
        };

        DownloadAgent agent = new DownloadAgent(
            mContext,
            "xp",
            "App_name",
            "http://www.google.com/some.apk",
            listener);
        agent.start();
        try {
            signal.await(100, TimeUnit.SECONDS);
            Assert.assertTrue(onStartTriggered);
            Assert.assertTrue(onProgressUpdateTriggered);
            Assert.assertTrue(onEndTriggered);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Here I add 3 class variables onStartTriggered, onProgressUpdateTriggered, onEndTriggered to track whether each method is successfully called. And in try catch block, use Assert.assertTrue to make sure these variables were accessed by correspondent methods after signal.await call. But unfortunately, this time I am not lucky enough. The test case failed. After digging into the code, I found these 3 methods were not called at all. That was tricky, they can be successfully called in the application code, but not in the test! With a few Google searches, I found this diagram on StackOverflow. Thread: Intent resolved to different process when running Unit Test in Android

Instrumentation runs all of your application components in the same process.

The diagram explained the phenomenon to some extent. The test runner is running in a separate thread than the UI thread. What this hint is that the test runner thread has no looper, which is required for Android message communication mechanism. See android.os.Looper.

As it infers, the runner thread has no looper, thus IDownloadListener listener, which was implemented using the Messager in android to receive messages from downloading process, cannot receive messages. As a result, in the test code, the above 3 methods were not called.

But then how can we test that code? The solution is simple, move the listener code to UI thread. In fact, Android Test framework does support this. Testing on the UI thread.

As illustrated in the following code snippet:

public class DownloadAgentTest extends ActivityInstrumentationTestCase2<UtActivity> {
    private static final String LOG_TAG = DownloadAgentTest.class.getName();

    boolean onStartTriggered = false;
    boolean onProgressUpdateTriggered = false;
    boolean onEndTriggered = false;

    public DownloadAgentTest() {
        super("com.example", com.ut.UtActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        mContext = mActivity.getBaseContext();
    }

    private UtActivity mActivity;
    private Context mContext;

    @Test
    public void testDownloadAgent() {
        final CountDownLatch signal = new CountDownLatch(1);

        onStartTriggered = false;
        onProgressUpdateTriggered = false;
        onEndTriggered = false;

        mActivity.runOnUiThread(new Runnable() {
            public void run() {
                IDownloadListener listener = new IDownloadListener() {
                    @Override
                    public void onStart() {
                        Log.d(LOG_TAG, "onStart");
                        onStartTriggered = true;
                    }

                    @Override
                    public void onProgressUpdate(int progress) {
                        Log.d(LOG_TAG, "onProgressUpdate(" + progress + ")");
                        onProgressUpdateTriggered = true;
                    }

                    @Override
                    public void onEnd(int result, String file) {
                        onEndTriggered = true;
                        Log.d(LOG_TAG, "onEnd(" + result + ", " + file + ")");
                        Assert.assertTrue(result == 1);
                        Assert.assertTrue(file.contains("/sdcard/"));
                        signal.countDown();
                    }
                };

                DownloadAgent agent = new DownloadAgent(
                    mContext,
                    "xp",
                    "App_name",
                    "http://www.google.com/some.apk",
                    listener);
                agent.start();
            }
        });

        try {
            signal.await(300, TimeUnit.SECONDS);
            Assert.assertTrue(onStartTriggered);
            Assert.assertTrue(onProgressUpdateTriggered);
            Assert.assertTrue(onEndTriggered);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

This time it works fine.

Caution: Although Testing on the UI thread tells that you can use both @UIThreadTest annotation and mActivity.runOnUiThread(new Runnable() to run some code on UI thread, in the case I mentioned above, do not use the first solution. Our code use

signal.await(300, TimeUnit.SECONDS);

to synchronize. So if we put @UIThreadTest annotation, listener will not be executed, as await will block the main UI thread. The Looper is blocked, it cannot receive messages!