0

I have an application which displays data (posts) from a web API.

  • A background service syncs this data at some unknown time and saves it.
  • When visiting my main activity it loads this data and displays it in a RecyclerView
  • The loading is handled via a singleton class

I currently test the main activity as follows

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

@Test
public void testDataLoad() {
    int postsTotal = DataSingleton.getInstance().getPostsCount();
    ViewInteraction empty = onView(withId(R.id.empty_view));
    ViewInteraction recycler = onView(withId(R.id.recycler_view));
    if (postsTotal == 0) {
        empty.check(matches(isDisplayed()));
        recycler.check(matches(not(isDisplayed())));
    } else {
        empty.check(matches(not(isDisplayed())));
        recycler.check(matches(isDisplayed()));
        recycler.check(new RecyclerViewItemCountAssertion(greaterThan(postsTotal)));
    }
}

I know that this can't be the right way to write tests. I want to be able to test both with an empty data set and a non-empty set so that the if-else is two separate tests. The only way I think I can achieve it is to mock the data.

Is there another way? Can I use Mockito to make the MainActivity use mock data without modifying the production code? Is my only choice to make it inject either real or mocked data providers in place of my singleton?

Is it better to just uninstall and reinstall my app each time so there is no data to start with and then continue with real data testing?

Nick Cardoso
  • 20,807
  • 14
  • 73
  • 124

1 Answers1

0

Android Activity are heavyweight and hard to test. Because we don't have control over the constructor, it is hard to swap in test doubles.

The first thing to do is to make sure you are depending on an abstraction of the data-source rather than a concretion. So if you are using a singleton with a getPostsCount() method then extract an interface:

interface DataSourceAbstraction {
    int getPostsCount();
}

Make a wrapper class that implements your interface:

class ConcreteDataSource implements DataSourceAbstraction {
    @Override
    int getPostsCount() {
        return DataSingleton.getInstance().getPostsCount();
    }
}

And make the Activity depend on that rather than the concrete DataSingleton

DataSourceAbstraction dataSourceAbstraction;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super(savedInstanceState);
    injectMembers();
}

@VisibleForTesting
void injectMembers() {
    dataSourceAbstraction = new ConcreteDataSource();
}

You can now swap in a test double by subclassing and overriding injectMembers that has relaxed visibility. It's a bad idea do this in enterprise development, but there are less options in Android Activities where you don't control the constructor of the class.

You can now write:

DataSourceAbstraction dataSource;

//system under test
MainActivity mainActivity

@Before
public void setUp() {
    mockDataSource = Mockito.mock(DataSourceAbstraction.class);
    mainActivity = new MainActivity() {
        @Override
        void injectMembers() {
            dataSourceAbstraction = mockDataSource;
        }
    };
}
David Rawson
  • 20,912
  • 7
  • 88
  • 124