Thursday, June 18, 2015

Android TDD Series: Test-Driving Views Part 1 - Activities


In my previous post we walked through the initial project setup necessary for test-driving in Android.  In that post we also wrote our first test to show that Robolectric was configured correctly, and I mentioned that we would go into more detail about what that test was doing we got to testing in activities.

A Deeper Dive into Activities

So, here we are.  Let's revisit that test and go into detail about what it is actually doing.

package com.jameskbride;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowActivity;

import static org.junit.Assert.assertEquals;

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

    private MainActivity activity;

    @Before
    public void setUp() {
        activity = Robolectric.setupActivity(MainActivity.class);
    }

    @Test
    public void whenTheActivityIsCreatedThenTheContentViewShouldBeSet() {
        ShadowActivity shadowActivity = Shadows.shadowOf(activity);
        assertEquals(R.id.main, shadowActivity.getContentView().getId());
    }
}

Lets break this down.

Notice we have a member variable in the test for our MainActivity, activity, and a setUp() method, with this interesting line:

activity = Robolectric.setupActivity(MainActivity.class);

This line is performing some Robolectric magic, but lets take some of the mystique out of it and make it apparent what is happening.  First, if we dig into Robolectric.setupActivity() we discover what we're really doing is the following:

public static <T extends Activity> setupActivity(Class<T> activityClass) {
    return ActivityController.of(shadowsAdapter, activityClass).setup().get();
  }

The ActivityController is getting a handle on our Activity, and calling setup().get(). This is similar to the builder pattern, and get() is simply returning the Activity back to us. Let's take a look at the more interesting ActivityController.setup():

/**
   * Calls the same lifecycle methods on the Activity called by Android the first time the Activity is created.
   *
   * @return Activity controller instance.
   */
  public ActivityController<T> setup() {
    return create().start().postCreate(null).resume().visible();
  }

Now the magic has been revealed. All we are really doing here is walking through the lifecycle methods, in this case create(), start(), and resume() of the Activity to get it into the desired state (Note that "postCreate()" and "visible()" are not lifecycle events, but Robolectric helper methods.).

The example I showed earlier used Robolectric.setUpActivity(), however this does not give you very fine-grained control, as it always sets the activity in the onResume() event. However most of the testing you'll do around activities will be life-cycle based, or related to when Fragments are created, displayed or replaced (more on Fragments in the next installment). As such you'll want to use the ActivityController instead, as it gives you the ability to put your Activity in the correct state for the event that you care about.  Testing in this manner will look something like this:

    
    private ActivityController<Mainactivity> activityController;
    private MainActivity activity;

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

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

With this level of control you'll be able to use the ActivityController to put the activity in any state you need.  If you have functionality you need to test in Activity.onPause() simply chain calls to the ActivityController and perform a get() at the end:

        
     activityController = Robolectric.buildActivity(MainActivity.class);
     activity = activityController.create().start().postCreate(null).resume().visible().pause().get();

You may have noticed in the tearDown() method above that we are using the ActivityController to walk the activity through additional life-cycle events. This is important, as it insures that any clean-up you may need to do is performed.

Starting Activities and Services


Beyond life-cycle events, two other common task you'll need to perform include starting other activities or services.  Using Robolectic these are trivial to write tests for.  Let's look at starting an activity first.  Here is an example test you might write:

    
    private ActivityController<MainActivity> activityController;
    private MainActivity activity;

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

    @Test
    public void whenTheActionBarButtonIsPressedThenTheSecondActivityIsStarted() {
        ShadowActivity shadowActivity = Shadows.shadowOf(activity);

        shadowActivity.clickMenuItem(R.id.action_button);

        Intent startedIntent = shadowActivity.peekNextStartedActivity();
        assertEquals(SecondActivity.class.getName(), startedIntent.getComponent().getClassName());
    }

In this test we perform a click on an action bar button to fire another activity and use Robolectric to peek at the next started activity. Let's take a look at the production code:
    //Here we are in the MainActivity
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        switch(id) {
            case R.id.action_button:                
                startActivity(new Intent(this, SecondActivity.class));
                break;
            default:
        }

        return super.onOptionsItemSelected(item);
    }

Pretty simple, right? Similarly, we can test that a service has been started from our activity as well:
    @Test
    public void whenTheActionBarButtonIsPressedThenCustomServiceIsStarted() {
        ShadowActivity shadowActivity = Shadows.shadowOf(activity);

        shadowActivity.clickMenuItem(R.id.action_button);

        Intent startedIntent = shadowActivity.peekNextStartedService();
        assertEquals(CustomService.class.getName(), startedIntent.getComponent().getClassName());
    }

Again, we're using this very basic pattern, only this time with a service. Here is the production code:
//Here we are in the MainActivity
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        switch(id) {
            case R.id.action_button:                
                startService(new Intent(this, CustomService.class));
                break;
            default:
        }

        return super.onOptionsItemSelected(item);
    }



This is obviously just a brief introduction into testing activities in Android. As you can see though, most of the activity functionality centers around testing the life-cycle events. Next time I'll go into detail about testing Fragments, and showing their interactions with activities and how to test for that as well.

As usual if you have any questions don't hesitate to ask, and I'm always looking for feedback. Thanks!