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!
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 ;) )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With