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.
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):
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):
Next, the Fragment code (src/main/java/com/jameskbride/TextFragment.java):
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.
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 ActivityControlleractivityController;
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 "
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!