Wednesday, November 13, 2013

Unit testing Android Handler using Robolectric

I was looking for ways to do unit testing my Android code. Since Android JUnit need to be executed in emulator or real device, I want another way to run my tests outside these environments.

Then I use actual Java JUnit and Robolectric to test my code, but the problem of this approach is some code that is need to be ran inside Android environment cannot actually running while testing.

In my case, I use Handler from background Thread to sends Message back to caller Thread. I want to test that the callback method is actually executed on caller Thread but the Message is never be sent in testing because of the test environment.

Here is snippet of my TargetClass code.

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

public void process(TargetClassListener listener){

  //1 grab caller thread callback.
  mListener = listener;
  mCallbackHandler = new Handler(){
    @Override
    public void handleMessage(Message msg){
      super.handleMessage(msg);
      if(mListener != null){
        mListener.onSuccess();
      }
    }
  };

  //2 start processing in worker thread.
  HandlerThread ht = new HandlerThread("",android.os.Process.THREAD_PRIORITY_BACKGROUND){
    @Override
    public void run(){
      //1 do something.
      //2 notify onSuccess().
      Message msg = mCallbackHandler.obtainMessage();
      msg.what = TargetClassListener.CALLBACK_ON_SUCCESS;
      msg.sendToTarget();
    }
  };
  ht.start();

}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

From the code above, the background Thread (HandlerThread ht) will send Message to mCallbackHandler which will force caller Thread to execute handleMessage() and execute mListener.onSuccess().

While test is running, the callback method onSuccess() won't be executed because the Message cannot be sent in testing environment.

So, instead of validate test result in onSuccess() (which cannot be reached here), I change to validate Handler object.

Here is snippet from my test code.

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@Test
public void process_normalCase_willExecuteCallbackOnCallerThread(){

  //1 execute class under test.
  TargetClass tc = new TargetClass();
  tc.process(this);

  //2 wait for background thread to be done.
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    fail(e.toString());
  }

  //3 validate handler object.
  assertNotNull(tc.getCallbackHandler());
  ShadowHandler handler = (ShadowHandler) Robolectric.shadowOf(tc.getCallbackHandler());
  assertTrue(handler.hasMessages(CALLBACK_ON_SUCCESS));
  assertEquals(Thread.currentThread().getId(), handler.getLooper().getThread().getId());

}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

At the third step, I get the Handler object from target class and wrap it inside Robolectric's ShadowHandler. Then validate the Message that the handler contains and the Thread id that associated with its Looper.

If these test cases are passed, then I can guarantee that the TargetClass will execute the right callback on the right Thread.

It's something, isn't it?  :-)