Saturday, September 19, 2015

Android TDD Series: Test-Driving Views Part 2 - Fragments

Well, it's been more than two months since my last post in the series and I've really fallen off of this horse. The good news is that time away from the Android TDD series was well spent. I have just published my first Android app! Now it's time to get back on and get back to some Android TDD!

We started talking about testing views in Android, specifically how to test activities. While activities are certainly a core component, we actually want to minimize how much code we put in them. This is especially true for all view-related code, such as widget-population and listener functionality. Instead, a majority of this functionality should be placed in fragments. Fragments were introduced into the Android framework when it became apparent that activities would be become bloated and difficult the maintain, with a mishmash of life-cycle management, data population, and widget management. By moving all widget-based functionality (including listeners) we are able to more cleanly delineate the responsibilities for each class. Activities should focus on their life-cycle events and populating the view-model which will be used by the fragment, which will be responsible for managing the widgets and their listeners.

To Test Fragments...


You actually need activities.  This is unfortunate because when we unit test we strive to test classes in isolation as much as possible.  That being said it isn't too onerous to unit test fragments; we simply need an activity when we start the fragment.

For now, let's take a look at how to start a fragment test (src/test/java/com/jameskbride/TextFragmentTest.java):


package com.jameskbride;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.widget.TextView;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ActivityController;

import static org.junit.Assert.assertEquals;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class TextFragmentTest {

    private ActivityController activityController;
    private MainActivity activity;

    @Before
    public void setUp() {
        activityController = Robolectric.buildActivity(MainActivity.class);
        activity = activityController.create().start().visible().get();
    }

    @After
    public void tearDown() {
        activityController.pause().stop().destroy();
    }

    public void startFragment(FragmentActivity parentActivity, Fragment fragment) {
        FragmentManager fragmentManager = parentActivity.getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(fragment, null);
        fragmentTransaction.commit();
    }

    @Test
    public void whenTheFragmentViewIsCreatedThenTheViewShouldBePopulated() {
        TextFragment textFragment = TextFragment.newInstance();
        startFragment(activity, textFragment);

        TextView myTextView = (TextView)textFragment.getView().findViewById(R.id.my_text_view);
        assertEquals("Hello world!", myTextView.getText());
    }
}

This code is obviously simplified from what you would normally see.  The first thing we do is in the setUp() method, which is to build an Activity and get it into the correct life-cycle event (see my previous post).  Once we are into the actual test itself we have another required step: starting the Fragment.  We accomplish this by getting ahold of the FragmentManager (in this case we're using the SupportFragmentManager, which is actually recommended).  Once we have the FragmentManager we start a new FragmentTransaction and commit that transaction.

Now we have performed enough setup to perform the actual test.  In this case we are simply checking that the text in a TextView widget has been set when the Fragment is created. After we add enough code to get it to compile (create the TextFragment, add the factory method, newInstance(), and create a view which contains an id of "my_text_view" we can run the test via "./gradlew testDebug". This leaves with a failing test which is expecting "Hello world!". Let's get this test passing.

First, our view (src/main/res/layout/text_fragment_layout.xml):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/my_text_view"/>
</LinearLayout>


Next, the Fragment code (src/main/java/com/jameskbride/TextFragment.java):
package com.jameskbride;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class TextFragment extends Fragment{
    public static TextFragment newInstance() {
        return new TextFragment();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.text_fragment_layout, container, false);

        return root;
    }
}


Let's break this down. First, in our newInstance() method we have returned a TextFragment. Second, in the onCreateView method we inflate our view, text_fragment_layout.xml, which contains the TextView with our "Hello world!" string. The test should now pass.  This is just a basic example of how to unit test fragments in Android.

One thing to keep in mind is that just as activities can be tested in their various life-cycle events, so too can fragments.  If we need to to test code in onAttach() method we can simply call it directly.   The technique for testing Fragments is essentially the same as testing activities. 

Hopefully this has been a useful starting point, though one which has been a long while coming!



4 comments:

  1. Brilliant. I really enjoyed your series James. This must be the best tutorial on Robolectric 3.0 out there. Simple, clear and to the point. I hope you continue with Mockito implementation as mentioned in the first part of the of the series. Cheers.

    ReplyDelete
    Replies
    1. Hi Anon, thanks for the comment, and for the encouraging words :-) I'm glad this series of posts has been useful to someone! I've got the post on Mockito and Dagger in the hopper, it's coming soon so keep an eye out. Cheers!

      Delete
  2. This tutorials are really amazing,i just started learn ROBolectric three days back,Now i feel Comfortable to write Unit Test.But i need know how we can call api using roboelectric .

    Thank You James

    ReplyDelete
    Replies
    1. Hi Nisarg, thanks for reading, and thanks for your kind words! I'm glad these articles are turning out to be useful :-) What kind of API are you going to be calling? My next installment is on Dagger for Dependency Injection, and Mockito for mocking. I hope it will be helpful.

      Delete