Start UI testing with Dagger2 and Mockito

This article is the 12th day article of Android Advent Calendar 2016.

Hello. Since last year, I changed my class to an Android engineer and made Android apps at work. This time I will also write about the Android test using Dagger2.

DI using Dagger2

--Since another person's article about DI and Dagger is well organized, I will only introduce it here. --Reference: Monkeys can understand! Dependency Injection: Dependency Injection --Reference: Introduction to Dagger2

Why you need DI for testing

--In a simple example, consider a screen that displays the results of some API. Suppose you create a ServiceClass that hits the API, then creates an instance of that ServiceClass in Activity, calls it, and displays the result. --In this case, Activity will be in a state of depending on ServiceClass. If you want to test according to the response of API, Activity and ServiceClass are tightly coupled, so you can not test the display pattern of this Activity unless you change the response on the server side that returns API. ――That's where the idea of DI comes into play. In this case, do not create an instance of ServiceClass in the screen, and replace the response by injecting an instance of the implementation class from outside the screen. Since Activity becomes aware of only the interface, it is not necessary to be aware of what the implementation is. --Dagger2 is a library for DI container that realizes this mechanism for injection.

Test the screen that enables DI

--There are the following tests that involve the context of Android View. --Testing on JVM with Robolectric --Testing on a real machine using Espresso --This time, I will introduce a test to replace the module that the screen depends on using DI in both patterns. There is also a way to replace it by creating a DaggerComponent for testing, but this time I will try to DI the mock by the method using Mocito. --The sample will be a screen that just lists the List returned by ListDataDao like this. Test the cases with and without the data displayed on this screen.

Robolectric --Activity is just a crap, so write a test only for Fragment

ListFragmentTest


@RunWith(RobolectricTestRunner.class)
public class ListFragmentTest {

    @Mock
    ListDataDao mockDao;

    private ListFragment fragment;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        fragment = ListFragment.newInstance();
        fragment.dao = mockDao;
    }

    @Test
public void The list is displayed when there is data() {

        List<String> mockData = new ArrayList<>();
        mockData.add("test1");
        mockData.add("test2");
        mockData.add("test3");

        when(mockDao.getData()).thenReturn(mockData);

        SupportFragmentTestUtil.startFragment(fragment, DriverActivity.class);

        RecyclerView recyclerView = (RecyclerView) fragment.getView().findViewById(R.id.recycler_view);
        recyclerView.measure(0, 0);
        recyclerView.layout(0, 0, 100, 1000);

        TextView textEmptyView = (TextView) fragment.getView().findViewById(R.id.empty);
        TextView itemView1 = (TextView) recyclerView.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.list_item_text);
        TextView itemView3 = (TextView) recyclerView.findViewHolderForAdapterPosition(2).itemView.findViewById(R.id.list_item_text);

        assertThat(recyclerView.getVisibility(), is(View.VISIBLE));
        assertThat(recyclerView.getAdapter().getItemCount(), is(3));
        assertThat(itemView1.getText(), is("test1"));
        assertThat(itemView3.getText(), is("test3"));

        assertThat(textEmptyView.getVisibility(), is(View.GONE));
    }

    @Test
public void The list is not displayed when the data is empty() {

        List<String> mockData = new ArrayList<>();

        when(mockDao.getData()).thenReturn(mockData);

        SupportFragmentTestUtil.startFragment(fragment, DriverActivity.class);

        RecyclerView recyclerView = (RecyclerView) fragment.getView().findViewById(R.id.recycler_view);
        TextView textEmptyView = (TextView) fragment.getView().findViewById(R.id.empty);

        assertThat(recyclerView.getVisibility(), is(View.GONE));
        assertThat(textEmptyView.getVisibility(), is(View.VISIBLE));
    }

    public static class DriverActivity extends AppCompatActivity implements HasComponent<ListActivityComponent> {

        @Override
        public ListActivityComponent getComponent() {

            ListActivityComponent activityComponent = mock(ListActivityComponent.class);

            ListFragmentComponent fragmentComponent = mock(ListFragmentComponent.class);

            when(activityComponent.plus(any(ListFragmentModule.class))).thenReturn(fragmentComponent);

            return activityComponent;
        }
    }
}

--Replace the Component referenced by Fragment in the Activity for Driver with a mock made with Mockito. With this, nothing happens even if the inject that injects the module is called by Fragment, but since dao remains null as it is, I will set it directly to Fragment at the time of setUp. ――At this time, you can change the behavior of dao for each test case by setting the dao of the mock made with Mockito.

Espresso

ListActivityUITest.java


@RunWith(AndroidJUnit4.class)
public class ListActivityUITest {

    @Mock
    ApplicationComponent applicationComponent;

    @Mock
    ListActivityComponent activityComponent;

    @Mock
    ListFragmentComponent fragmentComponent;

    @Mock
    ListDataDao mockDao;

    @Rule
    public ActivityTestRule<ListActivity> activityRule = new ActivityTestRule<>(ListActivity.class, true, false);

    @Before
    public void setUp() throws Exception {

        MockitoAnnotations.initMocks(this);

        SampleApplication app = (SampleApplication) InstrumentationRegistry
                .getTargetContext()
                .getApplicationContext();

        app.setComponent(applicationComponent);

        when(applicationComponent.plus(any(ListActivityModule.class)))
                .thenReturn(activityComponent);

        when(activityComponent.plus(any(ListFragmentModule.class)))
                .thenReturn(fragmentComponent);

        doAnswer(invocation -> {
            ListFragment fragment = (ListFragment) invocation.getArguments()[0];
            fragment.dao = mockDao;
            return fragment;
        }).when(fragmentComponent).inject(any(ListFragment.class));
    }

    @Test
public void The list is displayed when there is data() {

        List<String> mockData = new ArrayList<>();
        mockData.add("test1");
        mockData.add("test2");
        mockData.add("test3");

        when(mockDao.getData()).thenReturn(mockData);

        activityRule.launchActivity(new Intent());

        onView(withId(R.id.recycler_view))
                .check(matches(hasDescendant(withText("test1"))));
        onView(withId(R.id.recycler_view))
                .check(matches(hasDescendant(withText("test2"))));
        onView(withId(R.id.recycler_view))
                .check(matches(hasDescendant(withText("test3"))));

        onView(withId(R.id.empty))
                .check(matches(not(isDisplayed())));
    }

    @Test
public void The list is not displayed when the data is empty() {

        List<String> mockData = new ArrayList<>();

        when(mockDao.getData()).thenReturn(mockData);

        activityRule.launchActivity(new Intent());

        onView(withId(R.id.empty))
                .check(matches(isDisplayed()));

        onView(withId(R.id.recycler_view))
                .check(matches(not(isDisplayed())));
    }
}

--In the case of Espresso, the idea is the same, before the test runs, replace it with the component of the mock, disable the inject, and then insert the dao of the mock directly into the Fragment. --By writing like this in setup, you can insert the mock dao at the timing when the inject of FragmentComponent is called.

    doAnswer(invocation -> {
        ListFragment fragment = (ListFragment) invocation.getArguments()[0];
        fragment.dao = mockDao;
        return fragment;
    }).when(fragmentComponent).inject(any(ListFragment.class));

end

Recommended Posts

Start UI testing with Dagger2 and Mockito
Sample code for basic mocking and testing with Mockito 3 + JUnit 5
Testing with com.google.testing.compile
Testing model with RSpec
Start k3s with docker-compose
Starting with Swift Swift UI
Testing request sending and receiving logic with MockWebServer in JUnit
Java: Start WAS with Docker and deploy your own application