Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Configuration change using Robolectric

To retain my AsyncTasks across configuration changes, I use a fragment-based solution with setRetainInstance(true), which hosts each AsyncTask and calls back to a listening Activity, similar to this solution http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html

Ultimately, the purpose is to test the AsyncTask's retention functionality throughout configuration changes using Robolectric, but I need to start with setting up the actual configuration change correctly. However, it seems I can't mimic the exact reference behavior that occurs during a configuration change.


Real app: When running a real app, on configuration change, the Activity is destroyed and recreated while the Fragment is retained, so it seems to be working. I can see this by checking their references before and after the configuration change (example references used below):

  • Real app, before: Activity: abc Fragment: xyz

  • Real app, after: Activity: bca Fragment: xyz (properly retained and reattached)


Case 1: When running recreate() on the Activity in the Robolectric test, however, the Activity doesn't seem to have its instance properly recreated (despite the docs saying the method performs all the lifecycle calls):

mActivityController =
Robolectric.buildActivity(AsyncTaskTestActivity.class).attach().create().start().resume().visible();

mActivity = mActivityController.get();
mActivity.recreate();
  • Robolectric with recreate(), before: Activity: abc Fragment: xyz

  • Robolectric with recreate(), after Activity: abc Fragment: xyz

This leads me to believe that a new Activity instance isn't properly created and the reattachment functionality therefore hasn't happened in a real way.


Case 2: If I create the test based on individual lifecycle calls instead:

mActivityController = Robolectric.buildActivity(AsyncTaskTestActivity.class).attach().create().start().resume().visible();
mActivityController.pause().stop().destroy();
mActivityController = Robolectric.buildActivity(AsyncTaskTestActivity.class).attach().create().start().resume().visible();

In this version, it seems the Activity gets fully replaced from scratch, but so does also the Fragment:

  • Robolectric with separate lifecycle calls, before Activity: abc Fragment: xyz

  • Robolectric with separate lifecycle calls, after Activity: bca Fragment: yzx


It seems I'm either reusing the same Activity (case 1) or replacing everything with new instances, as if there is no underlying Application that retains the Fragment (case 2).

Question: is there any way I can set up my Robolectric test to mimic the reference result that I get when running the app in an actual Android environment (as per the Real app result), or am I stuck with either creating a separate test app or settling with Robotium functional tests? I tried to do it like this https://stackoverflow.com/a/26468296 but got the same result as my case 2.

Thanks in advance!

like image 485
jomni Avatar asked Mar 16 '15 20:03

jomni


1 Answers

I have played around a bit and came up with a solution using Robolectric 3.0 and Mockito:

@RunWith(RobolectricGradleTestRunner.class) 
@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.KITKAT, shadows = {ExampleActivityTest.ExampleActivityShadow.class})
public class ExampleActivityTest {

    @Mock
    private FragmentManager fragmentManagerMock;

    @Before
    public void setup() {
        initMocks(this);
        setupFragmentManagerMock();
    }

    @Test
    public void testRestoreAfterConfigurationChange() {
        // prepare
        ActivityController<ExampleActivity> controller = Robolectric.buildActivity(ExampleActivity.class);
        ExampleActivity activity = controller.get();
        ExampleActivityShadow shadow = (ExampleActivityShadow) Shadows.shadowOf(activity);
        shadow.setFragmentManager(fragmentManagerMock);

        ActivityController<ExampleActivity> controller2 = Robolectric.buildActivity(ExampleActivity.class);
        ExampleActivity recreatedActivity = controller2.get();
        ExampleActivityShadow recreatedActivityShadow = (ExampleActivityShadow) Shadows.shadowOf(recreatedActivity);
        recreatedActivityShadow.setFragmentManager(fragmentManagerMock);

        // run & verify
        controller.create().start().resume().visible();

        activity.findViewById(R.id.inc_button).performClick();
        activity.findViewById(R.id.inc_button).performClick();

        assertEquals(2, activity.lostCount.count);
        assertEquals(2, activity.retainedCount.count);

        Bundle bundle = new Bundle();
        controller.saveInstanceState(bundle).pause().stop().destroy();
        controller2.create(bundle).start().restoreInstanceState(bundle).resume().visible();

        assertEquals(0, recreatedActivity.lostCount.count);
        assertEquals(2, recreatedActivity.retainedCount.count);
    }

    private void setupFragmentManagerMock() {
        final HashMap<String, Fragment> fragments = new HashMap<>();
        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                return fragments.get(invocation.getArguments()[0]);
            }
        }).when(fragmentManagerMock).findFragmentByTag(anyString());

        final HashMap<String, Fragment> fragmentsToBeAdded = new HashMap<>();
        final FragmentTransaction fragmentTransactionMock = mock(FragmentTransaction.class);
        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                fragmentsToBeAdded.put((String) invocation.getArguments()[1], (Fragment) invocation.getArguments()[0]);
                return fragmentTransactionMock;
            }
        }).when(fragmentTransactionMock).add(any(Fragment.class), anyString());
        doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                fragments.putAll(fragmentsToBeAdded);
                return null;
            }
        }).when(fragmentTransactionMock).commit();

        when(fragmentManagerMock.beginTransaction()).thenReturn(fragmentTransactionMock);
    }

    @Implements(Activity.class)
    public static class ExampleActivityShadow extends ShadowActivity {

        private FragmentManager fragmentManager;

        @Implementation
        public FragmentManager getFragmentManager() {
            return fragmentManager;
        }

        public void setFragmentManager(FragmentManager fragmentManager) {
            this.fragmentManager = fragmentManager;
        }
    }
}

Note that I have only mocked the methods of FragmentManager (beginTransaction() and findFragmentByTag()) and FragmentTransaction (add() and commit()) that I use in my code, so you might need to expand these depending on your code.

I haven't done too much work with Robolectric yet, so there may be a more elegant solution to this, but this works for me for now.

You can see the full source code and project setup here: https://github.com/rgeldmacher/leash (might be worth a look if you still need to retain objects ;) )

like image 144
ct_rob Avatar answered Oct 28 '22 18:10

ct_rob