androidunmarshallingonsaveinstancestateonrestoreinstancestateinstancestate

Espresso tests cause "ClassNotFoundException when unmarshalling" (REPRODUCED on basic app)


It seems that this unmarshalling exception always gets thrown when an activity is recreated with saved instance state during espresso tests. I have reproduced it with a very basic android app. Here are the steps:

  1. Create an android app with two activities which each have one button. The button on the first activity opens the second activity. The button on the second activity closes the current activity.

  2. Add an espresso test which simply opens the first activity, clicks the button (to open the second activity), then clicks the button on the second activity (to finish the second activity and go back to the first activity).

  3. On your emulator, be sure to enable "Don't Keep Activities" on your emulator.

In my real app, it varies based on the activity which class will be "unknown" to cause the unmarshalling. In this specific example, it's apparently the toolbar. I have found that by removing specific entries ("androidx.lifecycle.BundlableSavedStateRegistry.key" and "android:viewHierarchyState") from the saved instance state that it will workaround this exception during espresso tests, but of course then things don't get restored correctly. And I'll reiterate that this is only a problem while running espresso tests. When performing the same exact test steps manually, everything unmarshalls correctly and there are no exceptions.

Changing sdk versions doesn't seem to help either.

That's it.

Here's all the gory code:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
//        // This will cause the exception to not be thrown during espresso tests
//        if(savedInstanceState != null) {
//            savedInstanceState.remove("androidx.lifecycle.BundlableSavedStateRegistry.key");
//            savedInstanceState.remove("android:viewHierarchyState");
//        }
        

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.open_button).setOnClickListener(v -> startActivity(new Intent(MainActivity.this, ChildActivity.class)));
    }
}
public class ChildActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_child);

        findViewById(R.id.close_button).setOnClickListener(v -> finish());
    }
}
@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    @Rule
    public ActivityTestRule<MainActivity> mActivityTestRule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void test() {
        onView(withId(R.id.open_button)).perform(click());
        onView(withId(R.id.close_button)).perform(click());
        onView(withId(R.id.open_button)).check(matches(isDisplayed()));
    }
}
androidx.test.espresso.PerformException: Error performing 'single click - At Coordinates: 115, 272 and precision: 16, 16' on view 'view.getId() is <2131231192/com.example.myapplication:id/close_button>'.
    at androidx.test.espresso.PerformException$Builder.build(PerformException.java:1)
    at androidx.test.espresso.base.PerformExceptionHandler.handleSafely(PerformExceptionHandler.java:8)
    at androidx.test.espresso.base.PerformExceptionHandler.handleSafely(PerformExceptionHandler.java:9)
    at androidx.test.espresso.base.DefaultFailureHandler$TypedFailureHandler.handle(DefaultFailureHandler.java:4)
    at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:5)
    at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:8)
    at androidx.test.espresso.ViewInteraction.desugaredPerform(ViewInteraction.java:11)
    at androidx.test.espresso.ViewInteraction.perform(ViewInteraction.java:8)
    at com.example.myapplication.MainActivityTest.mainActivityTest(MainActivityTest.java:31)
    ... 32 trimmed
Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: androidx.appcompat.widget.Toolbar$SavedState
    at android.os.Parcel.readParcelableCreator(Parcel.java:2839)
    at android.os.Parcel.readParcelable(Parcel.java:2765)
    at android.os.Parcel.readValue(Parcel.java:2668)
    at android.os.Parcel.readSparseArrayInternal(Parcel.java:3118)
    at android.os.Parcel.readSparseArray(Parcel.java:2351)
    at android.os.Parcel.readValue(Parcel.java:2725)
    at android.os.Parcel.readArrayMapInternal(Parcel.java:3037)
    at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:288)
    at android.os.BaseBundle.unparcel(BaseBundle.java:232)
    at android.os.Bundle.getSparseParcelableArray(Bundle.java:1010)
    at com.android.internal.policy.PhoneWindow.restoreHierarchyState(PhoneWindow.java:2133)
    at android.app.Activity.onRestoreInstanceState(Activity.java:1135)
    at android.app.Activity.performRestoreInstanceState(Activity.java:1090)
    at android.app.Instrumentation.callActivityOnRestoreInstanceState(Instrumentation.java:1317)
    at android.app.ActivityThread.handleStartActivity(ActivityThread.java:2953)
    at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
    at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
    at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at androidx.test.espresso.base.Interrogator.loopAndInterrogate(Interrogator.java:14)
    at androidx.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:8)
    at androidx.test.espresso.base.UiControllerImpl.loopUntil(UiControllerImpl.java:1)
    at androidx.test.espresso.base.UiControllerImpl.injectMotionEvent(UiControllerImpl.java:6)
    at androidx.test.espresso.action.MotionEvents.sendUp(MotionEvents.java:7)
    at androidx.test.espresso.action.MotionEvents.sendUp(MotionEvents.java:1)
    at androidx.test.espresso.action.Tap.sendSingleTap(Tap.java:5)
    at androidx.test.espresso.action.Tap.sendSingleTap$bridge(Unknown Source:0)
    at androidx.test.espresso.action.Tap$1.sendTap(Tap.java:3)
    at androidx.test.espresso.action.GeneralClickAction.perform(GeneralClickAction.java:6)
    at androidx.test.espresso.ViewInteraction$SingleExecutionViewAction.perform(ViewInteraction.java:2)
    at androidx.test.espresso.ViewInteraction.doPerform(ViewInteraction.java:25)
    at androidx.test.espresso.ViewInteraction.doPerform$bridge(Unknown Source:0)
    at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:2)
    at androidx.test.espresso.ViewInteraction$1.call(ViewInteraction.java:1)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:193)
    at android.app.ActivityThread.main(ActivityThread.java:6669)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Solution

  • From what I can tell, you're supposed to instead use ActivityScenario and then test the different activities' states using ActivityScenario.moveToState or ActivityScenario.recreate.