Sunday, 17 May 2015

How to test AsyncTask in Android

In Android, test is not as easy as any other platform. Because Android test cannot be run without emulator. Particulary when it comes to AsyncTask or Service, it is difficult to test because they are different type of thread and hard to check their result. Then, how can we ensure the result of AsyncTask valid?

AsyncTask is a thread and an asynchnorous as the name means. So, we need to wait for it finishes its job and need to capture the event. Then, when it happens in AsyncTask. It can be one of onBackground() and onPostExecute() methods. It doesn't matter you use onBackground() or onPostExecute() but I prefer onPostExecute(). Anyway, we can test an AsyncTask if we can hook it. Then, how can we hook it? For that, we can use callback pattern. But we need to delay main thread to wait for the AsyncTask's job done because we want to check the result.

So the structure for the test would be like:

1. Create AsyncTask A
2. Injection a callback into A 
3. Wait until A finish
4. Invoke callback in A.postExecute()
5. Verify the result
We can use java.util.concurrent.CountDownLatch for synchronising more than one thread. CountDownLatch is a synchronisation aid that allows one or more threads to wait until a set of operations being performed in other threads completes. Here are some methods in CountDownLatch class we are going to use for the test:
await() Causes the current thread to wait until the latch has counted down to zero, unless the thread is interrupted.
countDown() Decrements the count of the latch, releasing all waiting threads if the count reaches zero.
We can await() method to wait for AsyncTask and countDown() to stop the blocking. Then, let's create a test.

We are going to use http://www.bbc.co.uk/radio1/playlist.json to get some JSON data. Network operation is very common and useful case for AsyncTask. I'd like to get the JSON and parse and display it in a list view.

First create AyncTask class to get the JSON.
import android.os.AsyncTask;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

/**
 * Created by Mark Sunghun Park
 */
public class JsonGetTask extends AsyncTask {

    private static final String TAG = "JsonGetTask";

    private JsonGetTaskListener mListener = null;
    private Exception mError = null;

    @Override
    protected String doInBackground(String... params) {

        String content = null;
        try {
            content = getJson(params[0]);
        } catch (RuntimeException e){
            mError = e;
        }

        return content;
    }

    public JsonGetTask setListener(JsonGetTaskListener listener) {
        this.mListener = listener;
        return this;
    }

    @Override
    protected void onPostExecute(String s) {
        if (this.mListener != null)
            this.mListener.onComplete(s, mError);
    }

    @Override
    protected void onCancelled() {
        if (this.mListener != null) {
            mError = new InterruptedException("AsyncTask cancelled");
            this.mListener.onComplete(null, mError);
        }
    }

    private String getJson(String address){
        try {
            URL url = new URL(address);
            URLConnection conn = url.openConnection();

            StringBuffer sb = new StringBuffer();
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = null;
            while ((line = br.readLine())!= null){
                sb.append(line);
            }

            br.close();
            return sb.toString();
        } catch (MalformedURLException e) {
            e.printStackTrace();
            throw new IllegalArgumentException("Invalid URL");
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Network error");
        }
    }


    public static interface JsonGetTaskListener {
        public void onComplete(String jsonString, Exception e);
    }
}

I think that it would be loose coupling if you do not put too many code in an AsyncTask. You may show progress dialog before executing an AsyncTask and dismiss it after job done. For this, you may pass context of an activity to the AsyncTask instance. But it makes for you to test it because your test rely on another Activity or context inherited object. I just set an interface to get callback from the AsyncTask. You can make it less-coupling using a listener.

Here is the test code:

import android.app.Application;
import android.test.ApplicationTestCase;
import android.text.TextUtils;

import java.util.concurrent.CountDownLatch;

public class ApplicationTest extends ApplicationTestCase {

    String mJsonString = null;
    Exception mError = null;
    CountDownLatch signal = null;

    public ApplicationTest() {
        super(Application.class);
    }

    @Override
    protected void setUp() throws Exception {
        signal = new CountDownLatch(1);
    }

    @Override
    protected void tearDown() throws Exception {
        signal.countDown();
    }

    public void testAlbumGetTask() throws InterruptedException {

        JsonGetTask task = new JsonGetTask();
        task.setListener(new JsonGetTask.JsonGetTaskListener() {
            @Override
            public void onComplete(String jsonString, Exception e) {
                mJsonString = jsonString;
                mError = e;
                signal.countDown();
            }
        }).execute("http://www.bbc.co.uk/radio1/playlist.json");
        signal.await();

        assertNull(mError);
        assertFalse(TextUtils.isEmpty(mJsonString));
        assertTrue(mJsonString.startsWith("{\"playlist\""));
        assertTrue(mJsonString.endsWith("}"));

    }
}
new CountDownLatch(1) is passed int number. It is the number of times countDown() must be invoked before threads can pass through await(). If it is negative, it gives IllegalArgumentException. signal.await() waits before signal.countDown() is called. If you want set timeout, you can callawait(long timeout, TimeUnit unit) (eg. singal.await(10, TimeUnit.SECOND)) instead.

9 comments:

  1. CountDownLatch cause java.lang.IllegalMonitorStateException

    ReplyDelete
    Replies
    1. I don't recommend to use this way to test AsyncTask. It's much better to use Robolectric and I'm also using that.

      Delete
    2. can you make something like this on how to use roboelectric?

      Delete
  2. really help me..thank for the author

    ReplyDelete
  3. This article is very much helpful and i hope this will be an useful information for the needed one. Keep on updating these kinds of informative things...
    Mobile App Development Company
    Android app Development Company
    ios app development Company
    Mobile App Development Companies

    ReplyDelete
    Replies
    1. I think the test code is old school now. I'm looking at some chance to using RxJava/RxAndroid to support more fluent way to test. Also the getJson method in the AsyncTask should be mocked to avoid any dependency on server. Unit test should be executed without any coupling.

      Delete
  4. Nice it seems to be good post... It will get readers engagement on the article since readers engagement plays an vital role in every blog.. i am expecting more updated posts from your hands.
    Mobile App Development Company
    Mobile App Development Company in India
    Android app Development Company
    ios app development Company
    Mobile App Development Companies

    ReplyDelete
    Replies
    1. I've been doing many different roles, web, mobile, java and javascript backend. I haven't committed to Android very much in the mean time. I will try to do more.

      Delete
  5. Pretty article! I found some useful information in your blog, it was awesome to read, thanks for sharing this great content to my vision, keep sharing..
    Mobile App Development Company
    Android App Development Company

    ReplyDelete