Android Clean Code: Part 3
Learn more about the Clean Code Android mobile design pattern and unit testing your Android app piece by piece for code quality.
Join the DZone community and get the full member experience.
Join For FreeHaving understood how to test the Android Activity and Interactor in Part 1 and Part 2 of this series, let’s look at the Presenter and its testing.
What Is the Work of Presenter?
A) To change the data retrieved from Interactor according to the UI needs. Typically, you may want to decorate the data or derive a value from the set of data fetched by Interactor (for example, the total number of future trips in the list of the trips that contains both future and past trips).
B) Filter data, when needed.
C) Sort the data (default sorting), when needed
In this example, we use Presenter for decoration.
The date of travel is in the YYYY/MM/DD format when passed from Interactor; we need to show a message showing the days until departure if the trip is in the future.
If it is a past trip, it will show a message with the days since the departure.
Let’s look at the Presenter:
interface HomePresenterInput {
public void presentHomeMetaData(HomeResponse response);
}
public class HomePresenter implements HomePresenterInput {
public static String TAG = HomePresenter.class.getSimpleName();
public WeakReference<HomeActivityInput> output;
private Calendar currentTime;
@Override
public void presentHomeMetaData(HomeResponse response) {
// Log.e(TAG, "presentHomeMetaData() called with: response = [" + response + "]");
//Do your decoration or filtering here
HomeViewModel homeViewModel = new HomeViewModel();
homeViewModel.listOfFlights = new ArrayList<>();
if (response.listOfFlights != null) {
// create a viewmodel out of model
for (FlightModel fm : response.listOfFlights) {
FlightViewModel fvm = new FlightViewModel();
fvm.checkInStatus = fm.checkInStatus;
fvm.terminal = fm.terminal;
fvm.gate = fm.gate;
fvm.flightName = fm.flightName;
fvm.startingTime = fm.startingTime;
//Decoration
Calendar startingTime = getStartingTimeCalendar(fvm);
long daysDiff = getDaysDiff(getCurrentTime().getTimeInMillis(),startingTime.getTimeInMillis());
setDaysFlyDecorationText(fvm, daysDiff);
homeViewModel.listOfFlights.add(fvm);
}
output.get().displayHomeMetaData(homeViewModel);
}
}
private void setDaysFlyDecorationText(FlightViewModel fvm, long daysDiff) {
if(daysDiff >=0){
fvm.noOfDaysToFly = "You have " + daysDiff + " days to fly";
} else {
daysDiff =-daysDiff;
fvm.noOfDaysToFly = "It has been " + daysDiff + " days since you flew";
}
}
@NonNull
private Calendar getStartingTimeCalendar(FlightViewModel fvm) {
int year = Integer.parseInt(fvm.startingTime.substring(0,4));
int month = Integer.parseInt(fvm.startingTime.substring(5,7));
int day = Integer.parseInt(fvm.startingTime.substring(8,10));
Calendar startingTime = Calendar.getInstance();
startingTime.set(year,month-1,day,0,0,0);
return startingTime;
}
private long getDaysDiff(long startTime,long endTime) {
long msDiff = endTime - startTime;
long daysDiff = TimeUnit.MILLISECONDS.toDays(msDiff);
Log.e(TAG,"diff is "+ daysDiff);
return daysDiff;
}
public Calendar getCurrentTime() {
if(currentTime == null) return Calendar.getInstance();
return currentTime;
}
public void setCurrentTime(Calendar currentTime) {
this.currentTime = currentTime;
}
}
Presenter implements the PresenterInput interface. In this class, the methodpresentHomeMetaData
takes care of data decoration. It creates the view model from the model that is passed by the Interactor and passes it to the Activity to display the data. Most of the data is copied without any change to the view model from the model, except noOfDaysToFly, where the private methodsetDaysFlyDecorationText
does the calculation of the days to fly, based on the current date.
Once the view model is created, Presenter calls the Activity using the ActivityInput Interface.
The class member output of typeActivityInput
is aWeakReference
so that we don’t create circular references. These members were wired by Configurator which will be explained in a future post.
android-clean-code-generator generates these classes, which will be explained in a future post.
If you look at the code again, you will be able to understand the logic; it is straight forward.
Unit Testing
Let’s look at unit testing the code. We will start with the test to verify whether Activity is called with the right inputs. Should we need Activity class to test the presenter?
No, we should use the mock of Activity. Let us create the Mock first:
private class HomeActivityInputSpy implements HomeActivityInput {
public boolean isdisplayHomeMetaDataCalled = false;
public HomeViewModel homeViewModelCopy;
@Override
public void displayHomeMetaData(HomeViewModel homeViewModel) {
isdisplayHomeMetaDataCalled = true;
homeViewModelCopy = homeViewModel;
}
}
Let us create a test method to verify if the displayHomeMetaData is called with the right inputs:
@Test
public void presentHomeMetaData_with_vaildInput_shouldCall_displayHomeMetaData(){
//Given
HomePresenter homePresenter = new HomePresenter();
HomeResponse homeResponse = new HomeResponse();
homeResponse.listOfFlights = new FlightWorker().getFutureFlights();
HomeActivityInputSpy homeActivityInputSpy = new HomeActivityInputSpy();
homePresenter.output = new WeakReference<HomeActivityInput>(homeActivityInputSpy);
//When
homePresenter.presentHomeMetaData(homeResponse);
//Then
Assert.assertTrue("When the valid input is passed to HomePresenter Then displayHomeMetaData should be called",
homeActivityInputSpy.isdisplayHomeMetaDataCalled);
}
Now knowing that the next class in the unidirectional data flow is called as expected, let us test the decoration logic — present the number of days to fly instead of the date of travel.
One of the FIRST principles in writing successful test cases is that the test should be repeatable.
The value for `viewModel.noOfDaysToFly` is calculated based on the current date, the value for `viewModel.noOfDaysToFly` can change depending upon the time of unit test execution. We overcome that by setting the current time as (2017-May-30) in the test code and make the test performing consistently. We use the object ActivityInputSpy to verify the values of the view model.
@Test
public void verify_HomePresenter_getDaysDiff_is_CalcualtedCorrectly_ForFutureTrips(){
//Given
HomePresenter homePresenter = new HomePresenter();
HomeResponse homeResponse = new HomeResponse();
ArrayList<FlightModel> flightsList = new ArrayList<>();
FlightModel flight1 = new FlightModel();
flight1.startingTime = "2017/12/31";
flightsList.add(flight1);
homeResponse.listOfFlights = flightsList;
HomeActivityInputSpy homeActivityInputSpy = new HomeActivityInputSpy();
homePresenter.output = new WeakReference<HomeActivityInput>(homeActivityInputSpy);
//When - current time is set to 2017-May-30
Calendar currentTime = Calendar.getInstance();
currentTime.set(2017,5,30,0,0,0);
homePresenter.setCurrentTime(currentTime);
homePresenter.presentHomeMetaData(homeResponse);
//Then
// "It has been " + daysDiff + " days since you flew";
String ExpectedText = "You have " + "184" + " days to fly";
String ActualText = homeActivityInputSpy.homeViewModelCopy
.listOfFlights.get(0).noOfDaysToFly;
Assert.assertEquals("When current date is 2016/10/12 & Flying Date is 2016/10/31"
+"Then no of days should be 184",
ExpectedText,ActualText);
}
Like the above example, we can test each and every public method of the presenter. By this time, you may have realized how powerful the Android Clean Code design pattern is- it helps you unit test the app piece by piece, without hardwiring dependencies. Check the complete Presenter test class here.
I suggest you clone the example project, if not done already and before we close this exercise, run the test cases with code coverage(Android Studio → Menu →Run →Run with coverage). Check the code coverage of the Presenter class.
What’s next? We’ll talk about Router in the next post.
Opinions expressed by DZone contributors are their own.
Comments