MVP with Testing - Part 2 (Testing MVP Architecture)
Jul 23, 2018 • 11 Minute Read
Testing MVP Architecture
Test-driven development (TDD) is the de-facto standard in development industry. It's being said "If it is not tested, it's broken :- Bruce Eckel"
In the previous article MVP Architecture, we have covered the basics to incorporate MVP architecture and now this article will cover the testing of MVP architecture.
Testing MVP with Espresso and JUnit
This section is divided into two parts to test the MVP UI using an instrumentation test and business logic using a junit test. This section is focused on testing the project specific logic rather than testing framework working.
Testing MoviePresenter
To test the business logic of presenter, we required to mock the UI and repository components so the primary focus will be on testing the presenter and its interaction with other components like view and repository instance. To apply mocking, we will use the mockito framework.
Setup
The unit test classes created under module-name/src/test/java/ package run on local JVM. These test are quick in nature and have no Android API interaction.
Create Test
To create either a local unit test or an instrumented test, you can create a new test for a specific class or method by following these steps:
- Open the Java file containing the code you want to test.
- Click the class or method you want to test, then press Ctrl+Shift+T.
- In the menu that appears, click Create New Test.
- In the Create Test dialog, edit any fields and select any methods to generate, then click OK.
- In the Choose Destination Directory dialog, click the source set corresponding to the type of test you want to create: androidTest for an instrumented test or test for a local unit test. Then click OK.
Test Presenter Functionalities
- Receive user event
- Initiate background thread to retrieve response
- Capture the response from the background thread using ArgumentCaptor
- Verify the behavior of the mocked view when a response is received
Explanation Step by Step
- Mock instances like view and movieRepo using @Mock annotation to test the complete functionality of presenter.
- Methods with @Before are used to setup the testing environment and dependencies, in this case, setup the mocking framework and initialize the presenter using mocked instances.
- Create an argumentCaptor instance to capture the callback interface reference and to provide response to mocked view instance.
- Create and pass a dummy list as response.
- Verify the successful method invocation of view instance.
MoviePresenterTest.java
public class MoviePresenterTest {
private static final Random RANDOM = new Random();
@Mock // 1
private MoviesListContract.View view;
@Mock // 1
private MovieRepo movieRepo;
private MoviePresenter presenter;
@Captor // 3
private ArgumentCaptor<MoviesListContract.OnResponseCallback> argumentCaptor;
@Before // 2
public void setUp(){
// A convenient way to inject mocks by using the @Mock annotation in Mockito.
// For mock injections , initMocks method needs to be called.
MockitoAnnotations.initMocks(this);
// get the presenter reference and bind with view for testing
presenter = new MoviePresenter(view,movieRepo);
}
@Test
public void loadMoviewList() throws Exception {
presenter.loadMoviewList();
verify(movieRepo,times(1)).getMovieList(argumentCaptor.capture());
argumentCaptor.getValue().onResponse(getList());
verify(view,times(1)).hideProgress();
ArgumentCaptor<List> entityArgumentCaptor = ArgumentCaptor.forClass(List.class);
verify(view).showMovieList(entityArgumentCaptor.capture());
assertTrue(entityArgumentCaptor.getValue().size() == 10);
}
@Test
public void OnError() throws Exception {
presenter.loadMoviewList();
verify(movieRepo,times(1)).getMovieList(argumentCaptor.capture());
argumentCaptor.getValue().onError("Error");
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
verify(view,times(1)).showLoadingError(argumentCaptor.capture()); // 3
verify(view).showLoadingError(argumentCaptor.getValue()); // 4
}
private List<Movie> getList() {
ArrayList<Movie> movies = new ArrayList<>();
try {
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "IT", Utility.convertStringToDate("2017-10-8"), 7.6, 127, Movie.Type.HORROR));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Jumanji 2", Utility.convertStringToDate("2018-12-20"), 8.3, 111, Movie.Type.ACTION));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "The Dark Knight", Utility.convertStringToDate("2008-07-08"), 9.0, 152, Movie.Type.ACTION));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Inception", Utility.convertStringToDate("2010-07-16"), 8.8, 148, Movie.Type.ACTION));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "The Green Mile", Utility.convertStringToDate("1999-12-10"), 8.5, 189, Movie.Type.DRAMA));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Transcendence", Utility.convertStringToDate("2014-04-18"), 6.3, 120, Movie.Type.FICTION));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Saving Private Ryan", Utility.convertStringToDate("1998-07-24"), 8.6, 169, Movie.Type.DRAMA));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Whiplash", Utility.convertStringToDate("2015-02-20"), 8.5, 117, Movie.Type.DRAMA));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "The Raid", Utility.convertStringToDate("2011-04-13"), 7.6, 111, Movie.Type.ACTION));
movies.add(new Movie(RANDOM.nextInt(Integer.MAX_VALUE), "Burnt", Utility.convertStringToDate("2015-10-30"), 6.6, 111, Movie.Type.DRAMA));
} catch (ParseException e) {
e.printStackTrace();
}
return movies;
}
}
Testing MovieListActivity
To perform automated testing on a UI, we will use Espresso framework to perform the instrumentation test. Espresso is not aware of any asynchronous operation or background thread. In order to make Espresso aware of your app's long-running operations, we will use CountingIdlingResource.
MoviePresent must increment the idling resource to make espresso aware of the idle time as shown below
@Override
public void loadMoviewList() {
view.showProgress();
mclient.getMovieList(callback);
// required for espresso UI testing
EspressoTestingIdlingResource.increment();
}
and decrement it when the response is received.
The idling resource is accessed in a static manner using a utility class EspressoTestingIdlingResource
public class EspressoTestingIdlingResource {
private static final String RESOURCE = "GLOBAL";
private static CountingIdlingResource mCountingIdlingResource =
new CountingIdlingResource(RESOURCE);
public static void increment() {
mCountingIdlingResource.increment();
}
public static void decrement() {
mCountingIdlingResource.decrement();
}
public static IdlingResource getIdlingResource() {
return mCountingIdlingResource;
}
}
Check the presenter code for complete implementation in repository
Explanation Step by Step
- Register the idling resource in the @Before annotated method to make Espresso aware of the asynchronous delay.
- Write a test to verify the visibility of the default empty message being used.
- Perform user action to initiate the data retrieval task.
- Check the visibility of the list on screen.
- Click on a particular list item.
- Verify the toast message, containing details of the movie.
MovieListTest.java
@RunWith(AndroidJUnit4.class)
public class MovieListTest {
@Rule
public ActivityTestRule<MoviesListActivity> activityTestRule =
new ActivityTestRule<>(MoviesListActivity.class);
/**
* Register IdlingResource resource to tell Espresso when your app is in an
* idle state. This helps Espresso to synchronize test actions.
*/
@Before // 1
public void registerIdlingResource() {
IdlingRegistry.getInstance().register(EspressoTestingIdlingResource.getIdlingResource());
}
/**
* Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
*/
@After
public void unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(EspressoTestingIdlingResource.getIdlingResource());
}
@Test // 2
public void checkStaticView() {
// verify default empty text message
onView(withId(R.id.swipe_msg_tv)).check(matches(isDisplayed()));
// |--------------------------|
//|----------------------------| find a view | using swipe_msg_tv id
//check visibility of view on screen <-------|
}
@Test
public void checkRecyclerViewVisibility() {
// 3. perform swipe
onView(withId(R.id.swipe_msg_tv)).check(matches(isDisplayed()));
onView(withId(R.id.swipe_container)).perform(swipeDown());
// verify swipe is displayed
onView(withId(R.id.swipe_msg_tv)).check(matches(not(isDisplayed())));
// 4 verify recycler view is displayed
onView(withId(R.id.movies_recycler_list)).check(matches(isDisplayed()));
// 5 perform click on item at 0th position
onView(withId(R.id.movies_recycler_list))
.perform(RecyclerViewActions.actionOnItemAtPosition(0, click()));
// 6 verify the toast text
MoviesListActivity activity = activityTestRule.getActivity();
onView(withText("Title : 'IT' Rating : '7.6'")).
inRoot(withDecorView(not(is(activity.getWindow().getDecorView())))).
check(matches(isDisplayed()));
}
}
Bonus
Robolectric : Robolectric is a testing framework which let you test the android framework components(activity, service etc) with any emulator.It's gives you control over the life cycle of components to test as well.
Continuous integration with circle ci and Travis: It is a software development practice in shared repository environment which allow developers/teams to focus on development with automated build system.
Google samples for android testing: Collection of testing examples for android by google.
I hope that testing will now help you to be a stellar programmer, you can share your love by giving this guide a thumbs up.