In part 2, you learned about the Model View Presenter (MVP) architecture. As a brief refresher, the MVP model is a popular choice when you want to write Android code that is easy to test. It is an architecture that keeps the business logic and user interface (UI) logic separate so each can be tested in isolation.
I believe one of the best ways to learn is by doing, so, for this post, we’ll be going over a simple app. Along the way, you’ll write code in the MVP format and then write tests for them. I’ll be using the same app from part 3 as the starting point for this example.
Simple App
The app that we’ll be building is quite simple. It shows information about one place, for this example it is Disneyland. When the user presses the button, it shows the information about Disneyland and that is it.
From part 3, you have already written the code for this simple app. However, it is in Model View Controller (MVC) architecture. The goal is to refactor the MVC codebase into a MVP one. The MVC codebase is available for you to use if you did not go over part 3. For reference, here is the refactored codebase in MVP (the result at the end of this post).
Refactor Simple App from MVC to MVP
The biggest difference between MVP and MVC is that the business logic and UI logic are isolated for MVP. So, how exactly can we do this? We can do this by having a presenter class that is pure Java (no Android SDK dependency) and let it communicate to the View (Activity) through an interface (contract).
MVC MainActivity Class
public class MainActivity extends AppCompatActivity { public void showDisneylandInfo(View view) { InputStream inputStream = getApplication().getResources().openRawResource(R.raw.disneyland); Place disneyland = Place.readFromStream(inputStream); updateTextViewWithPlaceInfo(disneyland); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setTitle("Place Info"); } private void updateTextViewWithPlaceInfo(Place place) { if (place == null) return; TextView placeInfoTextView = findViewById(R.id.placeInfoTextView); String placeInfo = place.name + "\n" + place.description; placeInfoTextView.setText(placeInfo); } }
This is the MVC MainActivity class where Java and Android components are stuck together. We need to separate them by moving all the pure Java code out to the presenter. The Android SDK dependent code must stay because that is the only way to use any Android APIs. Looking at the code you’ll notice that most of the code will stay except for the line where you instantiate the Place object in the function showDisneylandInfo(…).
Interface
For the presenter and UI view to communicate, there must be a way for the two to talk to each other. Creating an interface is a way to allow the presenter and UI view to talk to each other. A Java interface used under this context can be thought of as a “contract”. Both parties know what is being offered and know what to expect.
So, what must the presenter be able to tell the view? The presenter needs to let the view knows at least two things. To show an error and to show the information about a place.
Now for the reverse, what must the view be able to tell the presenter? Mainly one thing, that is to parse the input stream for a place.
Putting all the requirements together you end up with the following interface:
public interface MainActivityContract { /** * Allows presenter to talk to the view. */ interface View { void showPlaceInformation(String information); void showPlaceNotFound(); } /** * Allows the view to talk to the presenter. */ interface Listener { void parseInputStream(InputStream inputStream); } }
Presenter
Let’s create the presenter class by creating a new Java class object called MainActivityPresenter and put the file at where MainActivity is located. The presenter class must provide the implementations that it promises in the interface (contract). So, in this case, the presenter must implement the parsing of the input stream. Also, it must take in a reference to the UI view so it can communicate to the UI through the interface.
Here is one way how you can implement the MainActivityPresenter class:
public class MainActivityPresenter implements MainActivityContract.Listener { private final MainActivityContract.View view; public MainActivityPresenter(MainActivityContract.View view) { this.view = view; } @Override public void parseInputStream(InputStream inputStream) { Place disneyland = Place.readFromStream(inputStream); if (disneyland == null) { view.showPlaceNotFound(); return; } String disneylandInfo = placeInformation(disneyland); view.showPlaceInformation(disneylandInfo); } private String placeInformation(Place place) { String info = place.name + "\n" + place.description; return info; } }
MVP MainActivity Class
We need to modify the MainActivity class to work with the presenter. So, we need to instantiate a presenter object and pass in a reference to the view (itself). Implement both the function it promises to provide through the interface. Also, when we want to show the information about a place, we will get its input stream through the Android API like before. The difference is instead of parsing it directly in the MainActivity, we pass it to the presenter to parse because it does not require any Android APIs to parse.
Here is one way how you can implement the MainActivity class:
public class MainActivity extends AppCompatActivity implements MainActivityContract.View { private MainActivityPresenter presenter = null; public final String ERROR_MESSAGE = "Can't find the designated place"; public void showDisneylandInfo(View view) { InputStream inputStream = getApplication().getResources().openRawResource(R.raw.disneyland); presenter.parseInputStream(inputStream); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setTitle("Place Info"); presenter = new MainActivityPresenter(this); } private void updateTextViewWithPlaceInfo(Place place) { if (place == null) return; TextView placeInfoTextView = findViewById(R.id.placeInfoTextView); String placeInfo = place.name + "\n" + place.description; placeInfoTextView.setText(placeInfo); } @Override public void showPlaceInformation(String information) { TextView placeInfoTextView = findViewById(R.id.placeInfoTextView); placeInfoTextView.setText(information); } @Override public void showPlaceNotFound() { TextView placeInfoTextView = findViewById(R.id.placeInfoTextView); placeInfoTextView.setText(ERROR_MESSAGE); } }
Stubbing
Before moving into testing, I think it is good for you to know about stubbing. You may have noticed that our presenter class requires a MainActivityContract.View class, which is the MainActivity. So, if we are writing pure Java tests how can we get the MainActivity that is dependent on the Android SDK? The answer is we can’t, but we can stub it. You can think of a stub as a test double that mimics the dependency.
Writing Unit Tests for the Presenter
To write pure java unit tests for the presenter, we are going to use the Mockito library. To use the library, you need to add the dependency to your app gradle file.
testImplementation 'org.mockito:mockito-core:2.8.47'
Your gradle dependencies should look like the following when you’re done.
Creating Presenter Test Class
Click on the presenter’s name in the class declaration and press CTRL + SHIFT + T and create a new class. Select it to be a test, not androidTest. Once Android studio generates the file add in a MainActivityContract.View and MainActivityPresenter object. We also need to define the setup method to use Mockito to stub a MainActivityContract.View for the presenter.
Here is the code to do this:
Note that any code that has the @Before annotation will run before each test. So, if you need to reset or initialize variables this is a good place to do it.
Writing Test Cases
We want to test the interface functions between the presenter and the view. So, we would want to test for when a place is not found and then when a place is found.
The first test you’ll write is for a place not found. This happens when the presenter tries to parse a null input stream. In addition, we also want to ensure that the proper function is being invoked. We can use Mockito to track how many times a function was invoked in a test. Here is one way to write the test that satisfies the requirements:
The second test is to test loading the information for a place (Disneyland). First, we need to open the Disneyland text file as an input stream and pass it into the presenter to parse. Once the presenter finishes parsing the input stream we want to analyze what happened with Mockito. For this test, we want to know if the showPlaceNotFound function was not called and that the showPlaceInformation function was called once. We also want to know if the information to show does match the Disneyland information in the source text file.
Here is one way to write the test that satisfies the requirements:
Once you’re done, run all the tests. You will not need a device or emulator, because the tests are running on the JVM (Java Virtual Machine). After the tests run, you will see the results in Android studio.
That is it, you have refactored MVC code to MVP and written tests for testing the presenter. Although this example was simple I hope you can see the benefits of using the MVP pattern.
If you found this post helpful, share it with others so they can benefit too.
What are your experiences with writing tests for Android? Do you find them to be helpful?
To get in touch, you can follow me on Twitter, leave a comment, or send me an email at steven@brightdevelopers.com.