I am trying to set up a basic test for a ListFragment and am running into serious issues. I'd been getting strange behavior out of Robolectric ListFragments, so I decided to run them on the device. They appear to be working precisely as I intended on the actual device, but not so with Robolectric. I'm using 2.3 snapshot 20140425.145412-162-jar-with-dependencies, because we need to use non-support Fragments.
When I run the ListFragment on the device, everything is dandy. When I run it on Robolectric, I get a null pointer exception at ListFragment$1.run(ListFragment.java:153)
. I've tried adding my own startFragment
method and using the one provided in FragmentTestUtil
.
This appears to be a bug, because even if I was doing something wrong I would expect behavior to be identical on the device.
Here is my ListFragment class:
public class TableManagerFragment extends ListFragment {
private static final String TAG = TableManagerFragment.class.getSimpleName();
/** All the TableProperties that should be visible to the user. */
private List<TableProperties> mTableList;
private TablePropertiesAdapter mTpAdapter;
public TableManagerFragment() {
// empty constructor required for fragments.
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "[onCreate]");
this.mTableList = new ArrayList<TableProperties>();
this.setHasOptionsMenu(true);
this.setMenuVisibility(true);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.d(TAG, "[onOptionsItemSelected] selecting an item");
return super.onOptionsItemSelected(item);
}
@Override
public View onCreateView(
LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
Log.d(TAG, "[onCreateView]");
View view = inflater.inflate(
R.layout.fragment_table_list,
container,
false);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// call this here because we need a context.
List<TableProperties> newProperties = this.retrieveContentsToDisplay();
Log.e(TAG, "got newProperties list of size: " + newProperties.size());
this.setPropertiesList(newProperties);
this.mTpAdapter = new TablePropertiesAdapter(this.getPropertiesList());
this.setListAdapter(this.mTpAdapter);
this.mTpAdapter.notifyDataSetChanged();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.table_manager, menu);
super.onCreateOptionsMenu(menu, inflater);
}
/**
* Retrieve the contents that will be displayed in the list. This should be
* used to populate the list.
* @return
*/
List<TableProperties> retrieveContentsToDisplay() {
TableProperties[] tpArray = TableProperties.getTablePropertiesForAll(
getActivity(),
TableFileUtils.getDefaultAppName());
List<TableProperties> tpList = Arrays.asList(tpArray);
return tpList;
}
/**
* Get the list currently displayed by the fragment.
* @return
*/
List<TableProperties> getPropertiesList() {
return this.mTableList;
}
/**
* Update the contents of the list with the this new list.
* @param list
*/
void setPropertiesList(List<TableProperties> list) {
// We can't change the reference, which is held by the adapter.
this.getPropertiesList().clear();
for (TableProperties tp : list) {
this.getPropertiesList().add(tp);
}
}
}
And fragment_table_list.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="#C0C0C0"
android:dividerHeight="1dp"
android:layout_weight="2"
android:drawSelectorOnTop="false" />
<TextView
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/no_data" />
</LinearLayout>
My Robolectric tests:
@RunWith(RobolectricTestRunner.class)
public class TableManagerFragmentTest {
private TableManagerFragment fragment;
private Activity parentActivity;
public void setupFragmentWithNoItems() {
this.fragment = getSpy(new ArrayList<TableProperties>());
doGlobalSetup();
}
public void setupFragmentWithTwoItems() {
TableProperties tp1 = mock(TableProperties.class);
TableProperties tp2 = mock(TableProperties.class);
when(tp1.getDisplayName()).thenReturn("alpha");
when(tp2.getDisplayName()).thenReturn("beta");
List<TableProperties> listOfMocks = new ArrayList<TableProperties>();
listOfMocks.add(tp1);
listOfMocks.add(tp2);
this.fragment = getSpy(listOfMocks);
doGlobalSetup();
}
/**
* Does the setup required regardless of what the fragment is returning.
*/
public void doGlobalSetup() {
ShadowLog.stream = System.out;
// We need external storage available for accessing the database.
TestCaseUtils.setExternalStorageMounted();
startFragment(this.fragment);
this.parentActivity = this.fragment.getActivity();
// Have to call visible to get the fragment to think its been attached to
// a window.
ActivityController.of(this.parentActivity).visible();
}
/**
* Get a mocked TableManagerFragment that will return toDisplay when asked to
* retrieve TableProperties.
* @param toDisplay
* @return
*/
private TableManagerFragment getSpy(List<TableProperties> toDisplay) {
TableManagerFragment spy = spy(new TableManagerFragment());
doReturn(toDisplay).when(spy).retrieveContentsToDisplay();
return spy;
}
@Test
public void emptyViewIsVisibleWithoutContent() {
setupFragmentWithNoItems();
// We aren't retrieving any TableProperties, so it is empty.
// Weirdly, the List is also visible. Perhaps this is because the list view
// is always visible, just not taking up any screen real estate if there
// are no elements? Should investigate this when we have known elements.
View emptyView = this.fragment.getView().findViewById(android.R.id.empty);
assertThat(emptyView).isVisible();
}
@Test
public void listViewIsGoneWithoutContent() {
setupFragmentWithNoItems();
View listView = this.fragment.getView().findViewById(android.R.id.list);
assertThat(listView).isGone();
}
@Test
public void emptyViewIsGoneWithContent() {
setupFragmentWithTwoItems();
View emptyView = this.fragment.getView().findViewById(android.R.id.empty);
assertThat(emptyView).isGone();
}
@Test
public void listViewIsVisibleWithContent() {
setupFragmentWithTwoItems();
View listView = this.fragment.getView().findViewById(android.R.id.list);
assertThat(listView).isVisible();
}
@Test
public void hasCorrectMenuItems() {
setupFragmentWithNoItems();
ShadowActivity shadowActivity = shadowOf(parentActivity);
Menu menu = shadowActivity.getOptionsMenu();
assertThat(menu)
.hasSize(4)
.hasItem(R.id.menu_table_manager_export)
.hasItem(R.id.menu_table_manager_import)
.hasItem(R.id.menu_table_manager_sync)
.hasItem(R.id.menu_table_manager_preferences);
}
}
And my failure trace:
java.lang.NullPointerException
at android.app.ListFragment$1.run(ListFragment.java:153)
at org.robolectric.util.Scheduler.postDelayed(Scheduler.java:37)
at org.robolectric.shadows.ShadowLooper.post(ShadowLooper.java:207)
at org.robolectric.shadows.ShadowHandler.postDelayed(ShadowHandler.java:56)
at org.robolectric.shadows.ShadowHandler.post(ShadowHandler.java:51)
at android.os.Handler.post(Handler.java)
at android.app.ListFragment.ensureList(ListFragment.java:432)
at android.app.ListFragment.onViewCreated(ListFragment.java:203)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:843)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1035)
at android.app.BackStackRecord.run(BackStackRecord.java:635)
at android.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1397)
at android.app.FragmentManagerImpl$1.run(FragmentManager.java:426)
at org.robolectric.util.Scheduler.postDelayed(Scheduler.java:37)
at org.robolectric.shadows.ShadowLooper.post(ShadowLooper.java:207)
at org.robolectric.shadows.ShadowHandler.postDelayed(ShadowHandler.java:56)
at org.robolectric.shadows.ShadowHandler.post(ShadowHandler.java:51)
at android.os.Handler.post(Handler.java)
at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1303)
at android.app.BackStackRecord.commitInternal(BackStackRecord.java:548)
at android.app.BackStackRecord.commit(BackStackRecord.java:532)
at org.robolectric.util.FragmentTestUtil.startFragment(FragmentTestUtil.java:14)
at org.opendatakit.tables.fragments.TableManagerFragmentTest.doGlobalSetup(TableManagerFragmentTest.java:60)
at org.opendatakit.tables.fragments.TableManagerFragmentTest.setupFragmentWithNoItems(TableManagerFragmentTest.java:38)
at org.opendatakit.tables.fragments.TableManagerFragmentTest.emptyViewIsVisibleWithoutContent(TableManagerFragmentTest.java:81)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:250)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:177)
at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)