Unit testing the unusual android components
By: Ramkishore V S
Tags: Android, Unit Testing, Android App Development, Tdd, AndroidDev

The Agenda

To start of with we know how important are unit tests. It helps us write better quality code. The manual testers and QA’s can save lot of their efforts. Unit tests are very handy to measure the test coverage and what not.

There came a lot of architectural patterns from the old MVC to the latest MVVM, MVI etc. The main trick behind all these architectures are to loosely couple and separate the business logics, API calls, database operations and the views so that everything can be independently tested.

So the general use cases cover business logics, API calls, database operations and the views. What about all of our other android components? As we all know android components can be classified as four namely,

Activities, Services, Broadcast Receivers, Content Providers.

If we look at it deeply we would notice that a lot of us including me aren’t giving much attention to unit testing broadcast receivers and services as it’s all system driven events. In this part we will majorly focus on unit testing the broadcast receivers and why its safe to test despite it being system driven.

Example use case

Let’s start of with a simple example.

public class OutgoingCallReceiver extends BroadcastReceiver {

@Override

public void onReceive(final Context context, Intent intent) {



final String phoneNum = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);



AsyncTask.execute(new Runnable() {

@Override

public void run() {

SharedPreferences preferences = context.getSharedPreferences("MyPrefs",

Context.MODE_PRIVATE);

SharedPreferences.Editor editor = preferences.edit();

editor.putString("phone", phoneNum);

editor.commit();

}

});



}



}

That’s a simple receiver that gets triggered whenever there is an outgoing call from our phone. That looks really fine why to unit test this?

Well we can only answer that question when we write test for that receiver. Let us do that.

Let us prepare our setUp() function and initialise all the objects required for testing. I am going to make use of Mockito for mocking objects wherever required.

public class OutgoingCallReceiverTest {



private OutgoingCallReceiver mOutgoingCallReceiver;

private Context mContext;



// Executes each task synchronously using Architecture Components.

@Rule public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();



@Before public void setUp() throws Exception {

mOutgoingCallReceiver = new OutgoingCallReceiver();

mContext = mock(Context.class);

}



}

2. Let us think of the happy case first happy case is our intent passed into the receiver has the data we expect. In that scenario let us explore if our code passes the test.

@Test public void test_receiver_all_intent() {

Intent intent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);

intent.putExtra(Intent.EXTRA_PHONE_NUMBER, mockPhoneNo);



mOutgoingCallReceiver.onReceive(mContext, intent);



AsyncTask.execute(new Runnable() {

@Override

public void run() {

SharedPreferences preferences = mContext.getSharedPreferences("MyPrefs",

Context.MODE_PRIVATE);



// Verify that the received phone no data is correct.

String phone = preferences.getString("phone",null);

assertEquals(mockPhoneNo, phone);

}

});



}

Now when I run this test it passes.

Happy case passes happily!

3. Now comes the case where I am not going to pass any intent data. Then let us see what happens to our receiver. So my test function is going to be like this,

@Test public void test_receiver_no_intent() {

mOutgoingCallReceiver.onReceive(mContext, null);

AsyncTask.execute(new Runnable() {

@Override

public void run() {

SharedPreferences preferences = mContext.getSharedPreferences("MyPrefs",

Context.MODE_PRIVATE);



// Verify that the received phone no data is correct.

String phone = preferences.getString("phone",null);

assertNull(phone);

}

});

}

So the expected result is this test should pass as we don’t have any phone no string set to our intent object as the intent itself is null. The phone that we try to retrieve from the preferences will of course return the default value we have set i.e; null. Let us run this and see.

Test case failed!

Oops ! The test case has failed. As I can see the logs the problem is because in our receiver we haven’t handled the null scenario of intent object. This is where TDD (Test Driven Development) comes into picture.

Let us go back to our receiver code and handle this and run our test again,

public class OutgoingCallReceiver extends BroadcastReceiver {

@Override

public void onReceive(final Context context, Intent intent) {

if (intent != null && intent.getExtras() != null) {

final String phoneNum = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);



AsyncTask.execute(new Runnable() {

@Override

public void run() {

SharedPreferences preferences = context.getSharedPreferences("MyPrefs",

Context.MODE_PRIVATE);

SharedPreferences.Editor editor = preferences.edit();

editor.putString("phone", phoneNum);

editor.commit();

}

});

}

}



}

Yay! .. We got this passed too

Now we have all cases covered. The coverage will be almost close to 100%. But that’s not where things come to an end. Broadcast receivers are really tricky.

By definition the system expects you to finish with the broadcast very quickly (under 10 seconds). Source : https://developer.android.com/guide/components/broadcasts

So we have to cover this scenario too. Luckily the android JUnit framework provides us timeout inbuilt object exceeding which the test case will automatically fail . We can use it like this,

@Test(timeout = 10000) public void test_receiver_all_intent() {}

@Test(timeout = 10000) public void test_receiver_no_intent {}

Summary

Thus we can clearly say we tend to miss out some conditions and important test scenarios in components like broadcast receivers and services too. In the next part we’ll explore how to handle various unit test scenarios in services.