Beginning Android Test Driven Development with Robolectric

One of my new year resolution for 2016 is to master Android Test driven development(TDD). This is easier said than done because it is not easy to implement or follow TDD in Android. One of the reasons TDD and testing in general is challenging in Android is mock or the lack thereof!. And who are we mocking here you may ask?

To understand the need for mock, you will need to remember that the things that make an Android app rock comes alive on the device. So if you want to test a given functionality or feature of your app, you have to run the app on the device to verify that the app produces the expected result. If you want to automate this process, that is if you do not want to manually run the app in the device in other to test it, then you will need to mimic the Android components that comes alive on the device such as Activity, Intent, Service etc.

The fake component that you create to mimic the real Android component is called a “Mock” or “Shadow” in Robolectric as you will see later in the tutorial. Mocks are just one tool you will need in your Android testing tool kit, there are other tools, terms and concepts you will need get familiarize with if you want to introduce testing in your Android development workflow. I will point some of them out in the course of this post.

What is Test Driven Development?

TDD is a software development approach whereby you write a test that describes an expected feature of your app before that feature is implemented. At first the test will fail, then after you implement the feature, the test will now pass. At this point you can refactor your app to behave in a meaningful way but still pass the test. It may be tempting to think that TDD is some kind of trial and error approach of development, while in reality you need to have a clear understanding of what you are building before you can write a meaningful test.

And just incase you are wondering what is Unit Testing – with unit testing, we test our app one event at a time as you will see in this tutorial. For example instead of testing whether the click of a button resulted in the creation of a Guest object, we can do a unit testing that checks if the click of the button fired an event in the first place. We can then do another unit test that checks for example if the correct Intent was started or if the correct event was posted to the event bus.
The sample project for this post is a Guest List App. This simple app, shows a list of guests. When you click on a guest you will be taken to a screen where you can check the guest in or check the guest out. There is a floating action button that when clicked will take you to another screen where you can add a guest. We will implement just enough of this app functionality to provide an adequate introduction to Android Test Driven Development with Robolectric.

Step 1 – Project Creation

  1. Create a new Android Studio project and select the defaults (API level 16, Blank Activity, Main Activity).
  2. For this post, I am using Gradle 1.5.0 and build tools 23.0.2 so you may want to update your Android Studio if you want to follow along.
  3. Next, you want to set your Test Artifact to Unit Test so that our Unit Tests will be included in the build.


Android Studio Build Variant

4. Ensure that Android Studio created a test directory for you as show below. If not you will need to open your project in project view and add a test folder.

Step 2 – Add Dependencies

All we had to do to start writing our unit tests is to pull in the dependency for Robolectric, testing is now fully supported in Android Studio. I recently read an excellent book on Android Test Driven development where whole two chapters were dedicated to setting up Robolectric testing in Android Studio, that is no longer the case. If you are getting started with Android development I highly recommend that book which is titled Android Activity (its in Beta) by @corey_latislaw

Add the following dependencies to your build.gradle file and rebuild, the one that had (Module: app) next to it. We will use the other libraries as we continue in this tutorial.

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    testCompile 'org.robolectric:robolectric:3.0'
    testCompile 'org.robolectric:shadows-support-v4:3.0'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:design:23.1.1'
    compile 'com.android.support:support-v4:23.1.1'
    compile 'de.hdodenhof:circleimageview:2.0.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.squareup:otto:1.3.8'
    compile 'com.google.code.gson:gson:2.5'
}

Source Code

The source code for this demo app is available, please show your support by sharing this post through any of your social media channel to get access to the source code.

[Locker] The locker [id=1996] doesn’t exist or the default lockers were deleted.
https://github.com/valokafor/GuestListApp[/sociallocker]

Step 3 – Write Your First Unit Test

According to the test driven development purists – thou shall not write a line of code unless you have first written a failing test. The question then is what do we test? And in what order?. The reality again is that you need a good understanding of the app you are building in other to determine what to test and in what order. This all comes with practice and in some cases you simply have to violate the commandment of test first development approach due to certain constraints that are unique to Android development.

It is best practice to greatly limit the code/logic we want to put in our Activity. Because of this, we will create a Fragment that will take on the responsibility of our Main Activity, we will call this Fragment AppStateFragment. Since we have not created this Fragment yet, it is therefore a good candidate for our first unit test. Since this Fragment is crucial to the app, we should write a unit test that verifies that it exists.

  1. In your test package add the Java class file called AppStateFragmentTest.java,
  2. It is a good practice to name your test class files after the name of the production class files that they are testing.
  3. Annotate your test class with Robolectric Gradle test runner which allows this code to be run natively in the Java Virtual Machine instead of the Android device.
@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)
public class AppStateFragmentTest {
}

4. Setup – your test class does not have artificial intelligence so we have to do some initialization for any test class that we create. Right click anywhere in your class, select Generate>Setup Method to add a setup method like this.

public class AppStateFragmentTest {

    @Before
    public void setUp() throws Exception {


    }

5. At the top of your test class declare a class member private AppStateFragment fragment , the IDE will give you a warning that this file does not exist and offer to create, go ahead and create that Fragment by yourself at the root of your project or create another package called fragments.

6. Now right click again in your test class and select Generate>Test Method and name that test method shouldNotBeNull like below. You will receive a req squiggly error on “assertNotNull” right click and do static import of import static junit.framework.TestCase.assertNotNull;

@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)
public class AppStateFragmentTest {
    private AppStateFragment fragment;

    @Before
    public void setUp() throws Exception {

    }

    @Test
    public void shouldNotBeNull() throws Exception {
        assertNotNull(fragment);
    }

}

7. Run the test by right clicking the method and selecting run and the test will fail, . Great! We ran a unit test. To make the test pass, create the fragment in the setup method. Run the test again after you instantiate the Fragment and the test should pass.

8. Now that our test has passed , we need to actually refactor our production code to implement the behavior that we are testing, this process if often referred to as Red, Green and Refactor. Add this factory method to the AppStateFragment.java class that returns an instance of the Fragment.

  public static AppStateFragment newInstance(){
        return new AppStateFragment();
    }

And this code to the MainActivity and call the method in your onCreate() method. Add the constant public static final String STATE_FRAGMENT_TAG = “GuestListState”; at the top of your MainActivity class.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    addStateFragment();
  }

private void addStateFragment() {
    if (getSupportFragmentManager().findFragmentByTag(STATE_FRAGMENT_TAG) == null) {
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.add(AppStateFragment.newInstance(), STATE_FRAGMENT_TAG);
        transaction.commit();
    }
}

Step 4 – Create Guest List Fragment

1. Add a Fragment named GuestListFragment.java to your fragments package and it should have a corresponding layout file named fragment_guest_list.xml

2. Also add GuestListFragmentTest.java to your test package. Add the Robolectric Test runner annotation like we did before and also add a setup method and a class member private GuestListFragment fagment to that class

3. Add a factory method that creates and returns GuestListFragment.java like this

 public static GuestListFragment newInstance() {
        return new GuestListFragment();
    }

4. Then in the AppStateFragment.java, update the oncreate method to create this Fragment like so

 @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        parentActivity = (MainActivity) getActivity();
        openFragment(GuestListFragment.newInstance(), "Guest List");
    }

    private void openFragment(Fragment fragment, String screenTitle){

        getActivity().getSupportFragmentManager()
                .beginTransaction()
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                .replace(R.id.container, fragment)
                .addToBackStack(null)
                .commit();
        parentActivity.getSupportActionBar().setTitle(screenTitle);
    }

5. At this point, you can add a shouldNotBeNull test to the GuestListFragmentTest.java like we did before and it should pass

Step 5 – Update Layout

1. Move the floating action button that the Android new project wizard added to activity_main.xml to the fragment_guest_list.java. Also add a listview to the GustListFragment layout file and this layout file will now look like this.

<RelativeLayout 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"
    tools:context=".fragments.GuestListFragment">

    <ListView
        android:id="@+id/guestListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"  />

    <!-- TODO: Update blank fragment layout -->
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_dialog_email" />

</RelativeLayout>

2. The MainActivity layout file will now look like this (add an id of container to the content_main)

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".core.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main"
        android:id="@+id/container"/>



</android.support.design.widget.CoordinatorLayout>

Step 6 – Add Unit Tests for ListView

We are using a listview to display the list of guests. We need to add a unit test that verifies that the listview is visible. And even if the listview is visible, it does us no good if that listview does not have an adapter associated with it so we also have to check if it has an adapter.

1. Add a test method to your GuestListFragmentTest.java with the name guestListShouldNotBeNull() like this (add static import for the Robolectic method “startFragment”

Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
@RunWith(RobolectricGradleTestRunner.class)
public class GuestListFragmentTest {
    private GuestListFragment fragment;
    private ListView guestListView;

    @Before
    public void setUp() throws Exception {
        fragment = GuestListFragment.newInstance();
        guestListView = (ListView) fragment.getView().findViewById(R.id.guestListView);
        startFragment(fragment); 

    }

    @Test
    public void shouldNotBeNull() throws Exception {
        assertNotNull(fragment);
    }

    @Test
    public void guestListShouldNotBeNull() throws Exception {
        assertViewIsVisible(guestListView);
        assertNotNull(guestListView.getAdapter());

    }
}

2. You should receive a red squiggly line that the IDE does not recognize assertViewIsVisible() test method, that it because it is a helper method that I added.

3. Create a class called TestHelper.java in your test package and add the method like so

    public static void assertViewIsVisible(View view){
        assertNotNull(view);
        assertThat(view.getVisibility(), equalTo(View.VISIBLE));
    }

4. Run the test and it should fail because the listview is not visible and the listview does not have an adapter yet.

Step 7 – Implement Adapter

1. To implement adapter add two packages to the root of your project: adapters and models

2. In the models package add a class file name Guest.java with the following content – use Android Studio to generate getters and setters

   private String Name;
    private String PhoneNumber;
    private String EmailAddress;
    private boolean IsCheckedIn;
    private String ProfileImagePath;

3. In the adapters package add the class file GuestListAdapter.java with following content

package com.okason.guestlist.adapters;

import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

import com.okason.guestlist.R;
import com.okason.guestlist.models.Guest;
import com.squareup.picasso.Picasso;

import java.util.List;


public class GuestListAdapter extends ArrayAdapter<Guest> {
    private List<Guest> mGuest;
    private Context mContext;

    public GuestListAdapter(Context context, List<Guest> guests) {
        super(context, R.layout.guest_list_custom_row);
        mGuest = guests;
        mContext = context;
    }

    @Override
    public int getCount() {
        return mGuest.size();
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;
        Holder mHolder;

        final Guest selectedGuest = mGuest.get(position);

        if (view == null){
            view = LayoutInflater.from(getContext()).inflate(R.layout.guest_list_custom_row, null);

            mHolder = new Holder();
            mHolder.guestName = (TextView)view.findViewById(R.id.text_view_guests_name);
            mHolder.guestEmail = (TextView)view.findViewById(R.id.text_view_guests_email);
            mHolder.guestHeadshot = (ImageView)view.findViewById(R.id.image_view_guest_head_shot);

            view.setTag(mHolder);
        }else {
            mHolder = (Holder)view.getTag();
        }

        mHolder.guestName.setText(selectedGuest.getName());
        Log.d("Adapter", selectedGuest.getName());
        mHolder.guestEmail.setText(selectedGuest.getEmailAddress());
        Picasso.with(mContext)
                .load(selectedGuest.getProfileImagePath())
                .fit()
                .placeholder(R.drawable.profile_icon)
                .into(mHolder.guestHeadshot);


        return view;
    }

    private class Holder{
        public ImageView guestHeadshot;
        public TextView guestName, guestEmail;
    }

}<span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: 16px; line-height: 1.5; background-color: #ffffff;"> </span>

4. Add the for Picasso and also add any image a place holder for Picasso and give it the name profile_icon.jpg

5. Add layout file for out listview named guest_list_custom_row.xml with the following content

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/image_view_guest_head_shot"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:layout_centerVertical="true"
        android:layout_margin="10dp" />

    <LinearLayout
        android:id="@+id/linear_layout_guest_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_margin="10dp"
        android:layout_weight="1"
        android:orientation="vertical"
        android:paddingTop="@dimen/margin_padding_normal">

        <TextView
            android:id="@+id/text_view_guests_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/text_view_guests_email"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>


</LinearLayout>

6. With this we can now setup the listview in GuestListFragment.java like this

public class GuestListFragment extends Fragment {
    private View mLayout;
    private ListView mListView;
    private GuestListAdapter mAdapter;
    private List<Guest> mGuests;

    public GuestListFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        mLayout = inflater.inflate(R.layout.fragment_guest_list, container, false);
        initializeView();
        return mLayout;
    }

    private void initializeView() {
        mListView = (ListView) mLayout.findViewById(R.id.guestListView);
        mGuests = new ArrayList<Guest>();
        mAdapter = new GuestListAdapter(getContext(), mGuests);
        mListView.setAdapter(mAdapter);
    }

    public static GuestListFragment newInstance() {
        return new GuestListFragment();
    }

}

7. Run the failing test again in GuestListFragmentTest.java and it should pass.

Step 8 – Populate ListView

Even though our test is passing, we still have a blank list so let us populate our list with some guests. First let us write a unit test that checks that our list have more than zero guest.

Add a test method named listViewShouldNotBeEmpty() to GuestListFragmentTest.java with the content below. Note that before we have to create a Shadow of our listview using Robolectric, and then we have to call the populateItems() method on this Shadow listview before we can get item count out of it.

  @Test
    public void listViewShouldNotBeEmpty() throws Exception {
        populateListView();
        assertTrue("Guest List is empty", guestListView.getAdapter().getCount() > 0);
    }

    private void populateListView() {
        assertNotNull(guestListView.getAdapter());
        ShadowListView shadowListView = Shadows.shadowOf(guestListView);
        shadowListView.populateItems();
    }

As expected, if run this test now, it will fail because our list is still empty. Go ahead and run the test listViewShouldNotBeEmpty(). You may get the error message “cannot access AndroidHttpClient”, if you do per this StackOverflow answer add useLibrary ‘org.apache.http.legacy’ to your gradle and the test should now run and fail.

cannot access AndroidHttpClient

To add some demo data:

1. Add a package named data to the root of you app.
2. Add a class named SampleData.java
3. In this class add a method named getSampleData() with the following content:

 public static List<Guest> getSampleGuests() {
        List<Guest> tempGuests = new ArrayList<Guest>();

        Guest guest1 = new Guest();
        guest1.setName("Debbie Sam");
        guest1.setEmailAddress("deb@email.net");
        guest1.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest1.JPG");
        tempGuests.add(guest1);


        Guest guest2 = new Guest();
        guest2.setName("Keisha Williams");
        guest2.setEmailAddress("diva@comcast.com");
        guest2.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest2.JPG");
        tempGuests.add(guest2);


        Guest guest3 = new Guest();
        guest3.setName("Gregg McQuire");
        guest3.setEmailAddress("emailing@nobody.com");
        guest3.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest3.JPG");
        tempGuests.add(guest3);


        Guest guest4 = new Guest();
        guest4.setName("Jamal Puma");
        guest4.setEmailAddress("jamal@hotmail.com");
        guest4.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest4.JPG");
        tempGuests.add(guest4);


        Guest guest5 = new Guest();
        guest5.setName("Dora Keesler");
        guest5.setEmailAddress("dora@yahoo.com");
        guest5.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest5.JPG");
        tempGuests.add(guest5);

        Guest guest6 = new Guest();
        guest6.setName("Anthony Lopez");
        guest6.setEmailAddress("toney@gmail.com");
        guest6.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest6.JPG");
        tempGuests.add(guest6);

        Guest guest7 = new Guest();
        guest7.setName("Ricardo Weisel");
        guest7.setEmailAddress("ricardo@gmail.com");
        guest7.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest7.JPG");
        tempGuests.add(guest7);

        Guest guest8 = new Guest();
        guest8.setName("Angele Lu");
        guest8.setEmailAddress("angele@ymail.com");
        guest8.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest8.JPG");
        tempGuests.add(guest8);


        Guest guest9 = new Guest();
        guest9.setName("Brendon Suh");
        guest9.setEmailAddress("brendon@outlook.com");
        guest9.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest9.JPG");
        tempGuests.add(guest9);


        Guest guest10 = new Guest();
        guest10.setName("Pietro Augustino");
        guest10.setEmailAddress("pietro@company.com");
        guest10.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest10.JPG");
        tempGuests.add(guest10);


        Guest guest11 = new Guest();
        guest11.setName("Matt Zebrotta");
        guest11.setEmailAddress("matt@stopasking.com");
        guest11.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest11.JPG");
        tempGuests.add(guest11);

        Guest guest12 = new Guest();
        guest12.setName("James McGiney");
        guest12.setEmailAddress("james@outlook.com");
        guest12.setProfileImagePath("https://dl.dropboxusercontent.com/u/15447938/attendanceapp/guest12.JPG");
        tempGuests.add(guest12);

        return tempGuests;
    }

4. Then update the initializeView() method in GuestListFragment.java to now call this method like so:

mGuests = SampleData.getSampleGuests();

5. Run the failing tests and it should now pass because we have more than zero items in the list

Summary

We have not come to the end of part 1 of this beginning Android Test Driven Development with Robolectric. In the next post we will add unit tests for the key functionality of the app which is check in and check out of guest. We will use Otto event bus to create events that we can now write the tests against. If you have not already subscribed to my email list please do so.

Leave me a comment to let know if you find the post useful or if you are struggling with a particular concept. We have barely scratched the surface of unit testing here and that is just one half of Android testing. I will write a future post once I wrap my head fully around the other side of Android testing which is integration testing or UI testing using Espresso.

For now, keep coding – code answereth all things!

Advertisements

About the Author valokafor

I am a Software Engineer with expertise in Android Development. I am available for Android development projects.

follow me on:

Leave a Comment: