user3359826
user3359826

Reputation:

Mocking Intent Extras in Espresso Tests

I’m trying to launch Activity in Espresso. The thing is I want to put mocked extras to the Intent I’m using to launch the Activity. Here is the example.

@RunWith(AndroidJUnit4.class)
public final class NiceActivityTester
{
    @Rule
    public final ActivityTestRule<NiceActivity> activityRule = new ActivityTestRule<>(NiceActivity.class, true, false);

    @Test
    public void justStartPlease() {
        NiceThing niceThing = Mockito.mock(NiceThing.class);
        Mockito.when(niceThing.getName()).thenReturn("Nice!");

        Intent intent = new Intent(InstrumentationRegistry.getTargetContext(), NiceActivity.class);
        intent.putExtra("NICE_THING", niceThing);

        activityRule.launchActivity(intent);
    }
}

Unfortunately unmarshalling Parcelable fails.

java.lang.RuntimeException: Unable to start activity ComponentInfo{app.application/app.application.activity.NiceActivity}: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: NiceThing_Proxy
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2325)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2387)
    at android.app.ActivityThread.access$800(ActivityThread.java:151)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1303)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5254)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: NiceThing_Proxy
    at android.os.Parcel.readParcelableCreator(Parcel.java:2295)
    at android.os.Parcel.readParcelable(Parcel.java:2245)
    at android.os.Parcel.readValue(Parcel.java:2152)
    at android.os.Parcel.readArrayMapInternal(Parcel.java:2485)
    at android.os.BaseBundle.unparcel(BaseBundle.java:221)
    at android.os.Bundle.getParcelable(Bundle.java:755)
    at android.content.Intent.getParcelableExtra(Intent.java:5088)
    at app.application.NiceActivity.getNiceThing(NiceActivity.java:40)
    at app.application.NiceActivity.setUpToolbar(NiceActivity.java:30)
    at app.application.NiceActivity.onCreate(NiceActivity.java:20)
    at android.app.Activity.performCreate(Activity.java:5990)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1106)
    at android.support.test.runner.MonitoringInstrumentation.callActivityOnCreate(MonitoringInstrumentation.java:534)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2278)

Is there any way to use mocked extras with Intent? Maybe some best practices regarding this area?

I really don’t want to make constructors for every model I’m using, some of these classes are quite complex with a dozen of fields. Spying on the extra or the Intent using Mockito doesn’t help.

Upvotes: 9

Views: 8598

Answers (3)

In recent updates, ActivityTestRule was declared a deprecated class. The Android Developers documentation recommends using ActivityScenario or ActivityScenarioRule.

Assuming that we are using ActivityScenarioRule and your test code as the base example, the solution to include an Intent would then be as follows:

@RunWith(AndroidJUnit4.class)
public final class NiceActivityTester
{
    @Rule
    public final ActivityScenarioRule<NiceActivity> scenarioRule = 
        new ActivityScenarioRule<>(
            new Intent(
                InstrumentationRegistry.getInstrumentation().getTargetContext(),
                NiceActivity.class)
                .putExtra("your_extra1_key", yourExtra1Value)            // first extra
                .putExtra("your_extra2_key", yourExtra2Value));          // second extra
                .putExtra("your_extra3_key", createParcelableObject())); // parcelable extra
                                                                         // you may add more

    // An example function that creates and returns a parcelable object
    ObjectType createParcelableObject() {
        ObjectType parcelableObject = new ObjectType();
        parcelableObject.setAttribute1("attribute1");
        parcelableObject.setAttribute2("attribute2");
        parcelableObject.setAttribute3("attribute3");
        .
        . // more attributes setting, for example
        .
        return parcelableObject;
    }

    @Test
    public void justStartPlease() {
        ActivityScenarioRule<NiceActivity> = scenarioRule.getScenario();

        // The rest would be your test code
        
    }
}

Here are a few things that you might want to take note of:

  • The getTargetContext, or technically, the InstrumentRegistry class itself, is now deprecated. The documentation recommends using ApplicationProvider. Therefore, instead of InstrumentationRegistry.getInstrumentation().getTargetContext(), you may replace with ApplicationProvider.getApplicationContext(). Either way, I have tested and verified that both work at the time of this answer's posting.
  • With ActivityScenarioRule, you are able to supply an Intent as its argument. Hence, you can include extras when launching your Activity. Simply create a new Intent and include the Activity that you intend to launch like the code sample above.
  • To successfully include a parcelable object to your intent, you have to include your object when instantiating your ActivityScenarioRule class/object. In the example code above, a function has been written to create and return a parcelable object while creating the ActivityScenarioRule. The documentation specifically mentions that the class "launches a given activity before the test starts and closes after the test". So, whatever it is that you need to include for your activity, do it when you're instantiating your ActivityScenarioRule.

Upvotes: 0

Carlos Daniel
Carlos Daniel

Reputation: 2729

Kotlin solution for this:

@get:Rule
val mActivityTestRule: ActivityTestRule<TheActivity> =
        object : ActivityTestRule<TheActivity>(TheActivity::class.java) {
            override fun getActivityIntent(): Intent {
                val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
                return Intent(targetContext, TheActivity::class.java).apply {
                    putExtra(TheActivity.ACTIVITY_TITLE,
                            targetContext.resources.getString(R.string.the_text))
                    putExtra(TheActivity.SETTING_TITLE,
                            targetContext.resources.getString(R.string.the_title))
                    putExtra(TheActivity.SCREEN_TYPE,
                            TheActivity.ScreenType.AMOUNT.name)
                }
            }
        }

Upvotes: 8

Max
Max

Reputation: 139

Use ActivityTestRule and override getActivityIntent

Example found here

public class MainActivityLaunchIntentTest {

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule =
            new ActivityTestRule<MainActivity>(MainActivity.class) {
                @Override
                protected Intent getActivityIntent() {
                    Context targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
                    Intent result = new Intent(targetContext, MainActivity.class);
                    result.putExtra("Name", "Earth");
                    return result;
                }
            };

    @Test
    public void shouldShowHelloEarth() {
        onView(withId(R.id.main__tv_hello)).check(matches(withText("Hello Earth!")));
    }
}

Upvotes: 12

Related Questions