Test-Driven Android: Tab Layouts

Reading Time: 3 minutes

Tab layouts indicate that there are multiple pages to look at, and users can swipe them or click them to move from page to page.

tabs

Their implementation makes them difficult to test drive, so in this post we will go over how to do it.

We implement the tabs in Android by placing a TabLayout element in our activity’s view, then getting ahold of it in the Activity class code and adding tabs to it. There are limited ways to get an instance of TabLayout.Tab, and because it is a final class, it cannot be mocked.

In the implementation, we get them by calling TabLayout’s newTab() method, then adding those tabs to the tab layout:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    TabLayout tabLayout = (TabLayout) findViewById(R.id.activity_tab_layout);
    tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.phone_icon));
    tabLayout.addTab(tabLayout.newTab().setIcon(R.drawable.heart_icon));

    RecentsFragment recentsFragment = RecentsFragment.newInstance(mUser);
    FavoritesFragment favoritesFragment = FavoritesFragment.newInstance(mUser);
    fragmentList.add(recentsFragment);
    fragmentList.add(favoritesFragment);

    ViewPager viewPager = (ViewPager) findViewById(R.id.activity_view_pager);
    tabLayout.setOnTabSelectedListener(injectedTabSelectedListener);
    injectedTabSelectedListener.setViewPager(viewPager);
    viewPager.setAdapter(injectedPagerAdapterProvider.providePagerAdapter(getSupportFragmentManager(), fragmentList));
    viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
}

We want to be able to test the tab layout, but Android and Robolectric provide us with few means to access information about the TabLayout or the tabs. We can get information about the TabLayout by extracting a method for the line that gets it out of the view (let’s call that method getTabLayout()) and overriding it in a class that subclasses our Activity and returns a mock tab layout:

protected TabLayout getTabLayout() {
    return (TabLayout) findViewById(R.id.activity_tab_layout);
}
public static class TestableActivity extends Activity {
    private TabLayout mockTabLayout;

    @Override
    protected TabLayout getTabLayout() {
        return mockTabLayout;
    }

    public void setMockTabLayout(TabLayout mockTabLayout) {
        this.mockTabLayout = mockTabLayout;
    }
}

But how are we going to get ahold of some tabs?

Without the ability to create a mock tab, I could not stub the mockTabLayout to return one. Also, because the mockTabLayout is a mock and not a real TabLayout, it would return null if I tried to call the newTab() method on it. The test below shows the solution that I found:

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

    when(mockTabLayout.newTab()).thenReturn(mockTab).thenReturn(otherMockTab);
    ActivityController<TestableActivity> controller = Robolectric.buildActivity(ActivityTest.TestableActivity.class);

    ActivityTest.TestableActivity testSubject = controller.get();

    testSubject.setMockTabLayout(mockTabLayout);
    controller.create();

    verify(mockTabLayout).addTab(mockTab);
    verify(mockTabLayout).addTab(otherMockTab);

    verify(mockTabLayout).setOnTabSelectedListener(mockTabSelectedListener);

    assertThat(mockTab.getIcon()).isEqualTo(subject.getResources().getDrawable(R.drawable.phone_icon));
    assertThat(otherMockTab.getIcon()).isEqualTo(subject.getResources().getDrawable(R.drawable.heart_icon));
}

So here, I create an instance of a real TabLayout in my test that is different from the mock I have put on my test subject. I call newTab() on it to create some tabs, then stub my mock TabLayout to return those tabs when called with the methods I call in the class under test.

The class under test calls methods on the real tabs to add their icons, and those methods are called on my tabs in the test, too.  Conveniently, since my test tabs are actual tabs, those tabs take the icon assigned to them in the class under test, so I can verify on the last two lines of the test that they have the icons they’re supposed to have.

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.