Test-Driven Android: Testing Context-Dependent Components with a Provider

It’s much easier to write a test-driven application when we can isolate the class under test, so its tests fail as a result of its behavior and not the behavior of its dependencies. Exercising the dependencies in tests—whether they’re collaborating classes or entire frameworks—increases the number of reasons that a test might be failing, which limits how helpful that test can be in diagnosing issues and driving out an implementation.

In Android, there’s an additional concern: exercising the Android framework in tests is slow. So it makes sense to exercise the framework as little as possible in tests. This gets complicated when we consider the way the Android framework is built, though. First of all, it works chiefly by allowing us to subclass Android classes and override life cycle methods. This is not a recipe for easy dependency injection. Furthermore, idiosyncratic implementations of Android sometimes involve obtaining dependencies (like views from the layout) via a method besides injection. (We talk about how to test dependencies in that scenario in this post).

But there’s another catch to decoupling and testing in Android: the launching of some components depends on passing in other components.

Take, for example, the FragmentStatePagerAdapter: to get one of these, we have to pass a SupportFragmentManager to its constructor. That SupportFragmentManager is context-dependent, which means it matters which context we get it from. To get one, we need to call getSupportFragmentManager() from a context. We can see an example of that in the following code, which pages between two fragments when the user switches tabs:

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

 setContentView(R.layout.activity_example);

 TabLayout tabLayout = getTabLayout();
 tabLayout.addTab(tabLayout.newTab().setText(R.string.one_tab));
 tabLayout.addTab(tabLayout.newTab().setText(R.string.another_tab));

 ViewPager viewPager = getViewPager();
 ExamplePagerAdapter examplePagerAdapter = examplePagerAdapterProvider.getPagerAdapter(this.getSupportFragmentManager());

 viewPager.setAdapter(examplePagerAdapter);
 viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
 }

We could new up a pager adapter by calling the FragmentStatePagerAdapter’s constructor in the activity with that activity’s SupportFragmentManager. Instead, though, we get our ExamplePagerAdapter from an ExamplePagerAdapterProvider. We create this class and inject it into our activity. The ExamplePagerAdapterProvider’s getPagerAdapter() method returns us an ExamplePagerAdapter when we call it with a SupportFragmentManager:

public class ExamplePagerAdapterProvider {
    public ExamplePagerAdapter getPagerAdapter(FragmentManager supportFragmentManager) {
        return new ExamplePagerAdapter(supportFragmentManager);
    }
}

In case you are curious, the ExamplePagerAdapter aids in switching between the two specific fragments presented during view paging:

public class ExamplePagerAdapter extends FragmentStatePagerAdapter{

    public ExamplePagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);
    }

    @Override
    public Fragment getItem(int position) {
        switch(position) {}
         case 0:
           return new FirstTabFragment();
           break;
         case 1:
           return new SecondTabFragment();
           break;
        }
    }

}

The provider gives us the opportunity to mock a return value in our test. This way, when getPagerAdapter() gets called on that provider, we can explicitly return a mock version of the ExamplePagerAdapter. Then, we can make sure that setAdapter() is called on our view pager with that specific mock adapter. I have put those lines in pink to make them easier to find in the test. The entire tab layout/pager adapter structure requires some mock-judo to test, and the other portions of this test demonstrate other techniques that got at the other lines of the implementation:

 @Test
    public void onCreate_setsTabLayoutAndViewPager() {
        mockTabLayout = mock(TabLayout.class);
        mockViewPager = mock(ViewPager.class);
        mockExamplePagerAdapter = mock(ExamplePagerAdapter.class);
        TabLayout tabFactory = new TabLayout(subject);
        mockTab = tabFactory.newTab();
        otherMockTab = tabFactory.newTab();

        when(mockTabLayout.newTab()).thenReturn(mockTab).thenReturn(otherMockTab);
        when(mockExamplePagerAdapterProvider.getPagerAdapter(testSubject.getSupportFragmentManager())).thenReturn(mockExamplePagerAdapter);

        ActivityController<TestableExampleActivity> controller = Robolectric.buildActivity(TestableExampleActivity.class);
        TestableExampleActivity testSubject = controller.get();
        testSubject.setMockTabLayout(mockTabLayout);
        testSubject.setMockViewPager(mockViewPager);
        controller.create();

        verify(mockTabLayout).addTab(mockTab);
        verify(mockTabLayout).addTab(otherMockTab);
        assertThat(mockTab.getText()).isEqualTo("FIRST TAB");
        assertThat(otherMockTab.getText()).isEqualTo("SECOND TAB");

        verify(mockViewPager).setAdapter(mockExamplePagerAdapter);
        ArgumentCaptor<TabLayout.TabLayoutOnPageChangeListener> captor = ArgumentCaptor.forClass(TabLayout.TabLayoutOnPageChangeListener.class);
        verify(mockViewPager).addOnPageChangeListener(captor.capture());
        TabLayout.TabLayoutOnPageChangeListener tabLayoutOnPageChangeListener = captor.getValue();

        assertThat(tabLayoutOnPageChangeListener).isEqualToComparingFieldByField(new TabLayout.TabLayoutOnPageChangeListener(mockTabLayout));
    }

It’s not the world’s clearest test: indeed, it may take some staring to fully understand. There may be strategies I have yet to learn that could help me write the test more clearly, or divide up the implementation such that the tests became smaller or more intention-revealing.

That said, this test accomplishes the goal of testing the activity’s behavior:

  • completely, and
  • independently of the other collaborating classes—even when those classes require context-specific parameters.

By accomplishing those goals, the test can help developers check that the code is functioning and prevent regressions during subsequent development. I feel safe adding features and performing refactors in the future because I know that my code is tested. So the fact that this test misses the goal of clearly documenting the code, I think, does not completely negate its utility.

Furthermore, until and unless more straightforward testing patterns emerge for Android, providers (and other testing techniques) offer a path forward. If we keep treading this path, then the artifacts along the way will start to look more familiar and less daunting.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s