Donn Felker

Not A Subscriber?

Join thousands of people who build and refuse to get left behind. One message a week on building, using AI as an accelerator, and staying sharp in the new AI frontier.

Receive one free message a week

January 6, 2014 Donn Felker

Robolectric ShadowTaskStackBuilder

I’m a proponent of unit testing and once you figure out how to use Robolectric its great for helping you unit test Android. That is, until you run into an instance where a Shadow class is not implemented and you get some obscure error while running the unit test. Such thing happened recently when trying to get some code under test.

The code previously used a Context#startActivitiy(…) to start the Activity. I’m building some deep linking framework for a client for their Android app and I needed to use the TaskStackBuilder in order to build the back tasks properly. The code looked similar to this:

PfmDispatcher.java

// Build the intent instance, etc
TaskStackBuilder t = TaskStackBuilder.create(context);
t.addNextIntentWithParentStack(intent);
t.startActivities();

View on GitHub

The code above starts and Activity and if the Activity defines a parent activity in the AndroidManifest.xml the entire task stack will be built and the activity will be started. My existing unit test to test this looked like this:

DeepLinkRouterTest.java

@Test
public void shouldBeAbleToGoToComments() {
Uri deepLink = Uri.parse("pfm:///user/frank/status/123/?version=88983845");
System.out.println(String.format("Testing %s", deepLink.toString()));
// The intent to start.
Intent expectedIntent = new Intent(Robolectric.application, Status.class);
expectedIntent.putExtra(Constants.Extras.STATUS_ID, 123L);
IntentCreator mockIntentCreator = mock(IntentCreator.class);
// Set up the stub
when(mockIntentCreator.newCommentsIntent(anyLong())).thenReturn(expectedIntent);
// Set up the router and run the code that will run the code path we want to test
DeepLinkRouterDouble router = DeepLinkRouterDouble.getInstance(Robolectric.application, Routes.class, new PfmDispatcher(Robolectric.application, mockIntentCreator), deepLink);
router.route();
// Assert that the intent returned goes to the welcome screen.
final Intent next = shadowOf(Robolectric.application).getNextStartedActivity();
ANDROID.assertThat(resolvedIntent).hasComponent(Robolectric.application, next);
ANDROID.assertThat(next).hasExtra(Constants.Extras.STATUS_ID);
// Verify that the intended intent creator method was indeed called.
verify(mockIntentCreator).newCommentsIntent(123L);
}

View on GitHub

The goal was to make sure the test above still passed when I moved from the traditional Context object to a TaskStackBuilder instance for starting the Activities. Unfortunately moving to use the TaskStackBuilder made the test fail with a framework exception. Robolectric does not have a ShadowTaskStackBuilder. So I built a very rudimentary one that would help my test pass, its not 100% perfect by any means but it should give you a jumping off point if you want to extend it as it needs a bit of work.

Writing the Shadow

ShadowTaskStackBuilder.java

package com.pfm.shadows;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.TaskStackBuilder;
import org.robolectric.Robolectric;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import static org.robolectric.bytecode.ShadowWrangler.shadowOf;
import static org.robolectric.Robolectric.directlyOn;
/**
* This is kind of a hack, The real task stack builder walks the Android Manifest and then creates
* the various child activities. I have not taken the time to figure out if we can
* manually instantiate the activities or not. This is a simple fake routine to work around.
*/
@SuppressWarnings({"UnusedDeclaration"})
@Implements(TaskStackBuilder.class)
public class ShadowTaskStackBuilder
{
private Context context;
private Intent intent;
@RealObject
TaskStackBuilder realTaskStackBuilder;
private void setup(Context context) {
this.context = context;
}
@Implementation
public static TaskStackBuilder create(Context context) {
TaskStackBuilder taskStackBuilder = Robolectric.newInstanceOf(TaskStackBuilder.class);
ShadowTaskStackBuilder stsb = (ShadowTaskStackBuilder) shadowOf(taskStackBuilder);
stsb.setup(context);
return taskStackBuilder;
}
@Implementation
public TaskStackBuilder addNextIntentWithParentStack(Intent intent) {
this.intent = intent;
return realTaskStackBuilder;
}
@Implementation
public void startActivities() {
context.startActivity(intent);
}
}

View on GitHub

When the test is run in Robolectric and configured to use this Shadow (as shown below) the code above will set up a new instance of TaskStackBuilder and use the Shadow to manage as the execution. In What we’re actually doing is avoiding all of the actual Implementation of TaskStackBuilder all together so we can get this test to pass. The code builds a TaskStackBuilder and then when the addNextIntentWithParentStack(Intent intent) method is called the intent is saved locally in the shadow. When the user calls startActivities on the TaskStackBuilder, we intercept that and simply call context.startActivity(intent). Since the context is an instance of Robolectric.application this works out great.

Is this perfect? By all means, no. But it will help someone who is stuck. How can it be improved? If you look at the source of TaskStackBuilder you’ll notice it delegates the activity creation and starting of them to a class called ContextCompat and it also uses IntentCompat. The source of ContextCompat reveals the usage of two other classes: ContextCompatJellybean and ContextCompatHoneycomb as well as some other code. As you can see, its a mini pandoras box under the hood. My implementation above avoids all of the code below and allow us to trust that TaskStackBuilder is doing what its supposed to.

Simply assuming that code will work is dangerous in practice you could do the following to ease your testing anxieties:

• Implement Shadows for all of those (have fun with that)
• Write a test that checks to manifest to ensure that parent activities are set in the AndroidManifest.xml
• Write integration tests that ensure that the up state is working.

Using these Shadow

To use the Shadow, create your test class and annotate it with the @Config annotation as shown below:

DeepLinkRouterTest_V2.java

@RunWith(RobolectricTestRunner.class)
@Config(shadows = { ShadowTaskStackBuilder.class }) // Robolectric will read this and use the shadow for this test file
public class DeepLinkRouterTest
{
@Test
public void shouldBeAbleToGoToComments() {
Uri deepLink = Uri.parse("pfm:///user/frank/status/123/?version=88983845");
System.out.println(String.format("Testing %s", deepLink.toString()));
// The intent to start.
Intent expectedIntent = new Intent(Robolectric.application, Status.class);
expectedIntent.putExtra(Constants.Extras.STATUS_ID, 123L);
IntentCreator mockIntentCreator = mock(IntentCreator.class);
// Set up the stub
when(mockIntentCreator.newCommentsIntent(anyLong())).thenReturn(expectedIntent);
// Set up the router and run the code that will run the code path we want to test
DeepLinkRouterDouble router = DeepLinkRouterDouble.getInstance(Robolectric.application, Routes.class, new PfmDispatcher(Robolectric.application, mockIntentCreator), deepLink);
router.route();
// Assert that the intent returned goes to the welcome screen.
final Intent next = shadowOf(Robolectric.application).getNextStartedActivity();
ANDROID.assertThat(resolvedIntent).hasComponent(Robolectric.application, next);
ANDROID.assertThat(next).hasExtra(Constants.Extras.STATUS_ID);
// Verify that the intended intent creator method was indeed called.
verify(mockIntentCreator).newCommentsIntent(123L);
}
}

View on GitHub

Thats it. You now have a very naive implementation of ShadowTaskStackBuilder to use in your application.