Android/Testing/Unit Testing/Monitor supermethods

Some monitoring behaviour is common: Activities can start other activities, for instance, and testing those calls involves recording the intents issued during the test. Robolectric offers this behaviour with its ShadowContextWrapper.getNextActivity, but the following may be a more useful strategy in certain circumstances.

Code under test edit

Activity that starts another activity.

class MyActivity extends Activity {
  ...
  protected void startSomeActivity() {
     startActivity(new Intent("start some activity");
  }
  ...
}

Test edit

Extend the activity under test, overriding the startActivity() method:

class MyActivityTest {
 class MonitoredMyActivity extends MyActivity implements MonitoringForStartActivity {
     List<Intent> startActivityCalls = new ArrayList<Intent>();

     @Override
     List<Intent> startActivityCalls() {
         return this.startActivityCalls;
     }

     @Override
     boolean startActivity(Intent i) {
         this.startActivityCalls.add(i);
     }
 }

 MonitoredMyActivity activity = new MonitoredMyActivity();

 @Test
 public void _TestActivityStarted() {
   activity.startSomeActivity();  
   assertThat(monitoredActivity, hasStartedActivity(intentMatching("start some activity")));
 }
}

Misc edit

This is a wrapping-up of being able to monitor an activity:

import android.content.Intent;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import java.util.Iterator;
import java.util.List;

/**
 * Matcher with a mix-in interface, to monitor 'startActivity()' calls.
 *
 * Boilerplate:
 *
 * class MonitoredMyActivity extends MyActivity implements MonitoringForStartActivity {
 *     List<Intent> startActivityCalls = new ArrayList<Intent>();
 *
 *     @Override
 *     List<Intent> startActivityCalls() {
 *         return this.startActivityCalls;
 *     }
 *
 *     @Override
 *     boolean startActivity(Intent i) {
 *         this.startActivityCalls.add(i);
 *     }
 * }
 *
 * assertThat(monitoredActivity, hasStartedActivity(sameInstance(someIntent)));
 */

public class MonitoringForStartActivityTypeSafeDiagnosingMatcher extends TypeSafeDiagnosingMatcher<MonitoringForStartActivityTypeSafeDiagnosingMatcher.MonitoringForStartActivity>
{
	public interface MonitoringForStartActivity
	{
		List<Intent> didCallStartActivity();
	}

	public static Matcher<MonitoringForStartActivity> hasNeverStartedActivity()
	{
		return new MonitoringForStartActivityTypeSafeDiagnosingMatcher();
	}

	public static Matcher<MonitoringForStartActivity> hasStartedActivity(final Matcher<Intent> matcher)
	{
		return new MonitoringForStartActivityTypeSafeDiagnosingMatcher(matcher);
	}

	public MonitoringForStartActivityTypeSafeDiagnosingMatcher(Matcher<Intent> ... matchers)
	{
		this.matchers = matchers;
	}

	private final Matcher<Intent>[] matchers;

	@Override
	protected boolean matchesSafely(MonitoringForStartActivityTypeSafeDiagnosingMatcher.MonitoringForStartActivity monitoredTestActivity, Description description)
	{
		final List<Intent> intents = monitoredTestActivity.startActivityCalls();
		description.appendText(" startActivity was called " + intents.size() + " times");
		if (intents.size() == 0)
		{
			description.appendText(" never");
		}
		else
		{
			description.appendText(" with");
			for (Intent intent : intents)
			{
				description.appendText(": " + intent);
			}
		}
		if (intents.size() != matchers.length)
		{
			return false;
		}
		boolean matchesAll = true;
		Iterator<Intent> eachIntent = intents.iterator();
		for (Matcher<Intent> matcher : matchers)
		{
			matchesAll = matchesAll && eachIntent.hasNext() && matcher.matches(eachIntent.next());
		}
		return matchesAll;
	}

	@Override
	public void describeTo(Description description)
	{
		if (matchers.length == 0)
		{
			description.appendText(" never calls startActivity");
		}
		else
		{
			description.appendText(" calls startActivity with ");
			for (Matcher<Intent> matcher : matchers)
			{
				description.appendDescriptionOf(matcher);
			}
		}
	}
}