Testing applications is hard, and testing Android Applications is harder. The natural way an Android Developer codes is completely untestable. We are born and molded into creating God classes – Activities and perform every bit of logic inside them. Some thought that introduction to Fragments will introduce a little bit of modularity, but we proved otherwise by shifting to God Fragments. When the natural urge of an Android Developer to
- apply logic,
- load UI,
- handle concurrency (hopefully, not using AsyncTask),
- load data – from the network; disk; and cache,
- and manage the state of the Activity/Fragment
finally, meets with a new form of revelation that he/she should test his/her application, all of the concepts acquired are shattered in a moment. The person goes to Android Docs to see why he started coding that way and realizes that even the system is flawed – Android Documentation is full of examples promoting God Activities, which they have used to show the use of API for only one reason – brevity. It’s not uncommon for us to steal some code from StackOverflow or Android Docs, but we don’t realize that they are presented there without an application environment or structure, all the required component just glued together to get it functionally complete, so that reader does not have to configure it anymore. Why would an answer from StackOverflow load a barcode scanner using a builder? Or build a ContentProvider to show how to query sorted data from SQLite? Or use a singleton for your provider classes? The simple answer is, they won’t. But this doesn’t mean you shouldn’t too.
The first thought that enters developer’s mind when he gets his hand on a piece of code is to paste it in the correct position and get it to work. There is always this moment of hesitation where conscience rings a bell saying, “What are you doing? Is it the right way to do it? How many lines till you stop overloading this Activity? It’s 2000 lines already”. But it dies as soon as we see the feature is working. And why test something which we have confirmed to work, right? I just wrote this code to show a progress bar while the data loads and hide it when it is done. I can see this working, what will I achieve in painfully writing a test for it, mocking the loading conditions and all. Wrong! Unit tests which test your trivial utils like Date Modification, String Parsing, etc are good and needed but are so trivial that they are hard to go wrong, and if they are, they are easy to fix as they have single usage throughout the app, and it is easy to spot bugs and fix them.
The real problem is testing of your app over dynamic conditions, where you have to emulate them so you can see if your app is making the right decisions. The progress bar working example may work for now, but what if it breaks over refactoring, or you wrote the same code elsewhere and forget to hide the progress bar? Simply copying a well-written test and changing 2-3 class names will fail the build and tell you what’s wrong. A well-contracted app can even contain tests to check that there’ll be no memory leaks. But none of this is possible if everything is jumbled into single Activity with callbacks, loaders, UI handling, business logic, etc. You can’t even think that where to begin. In this post, I will briefly discuss, how to design and test applications using MVP pattern.
MVP to the Rescue
Before moving ahead, I must put a disclaimer saying that MVP is a design pattern which follows a very opinionated implementation. The extent to which you want to refactor your app and the number of abstractions you are willing to do are in your hand. There aren’t any golden rules where your implementation will fail to be called as MVP. Remember, the client doesn’t care about architecture, it’s for you, so whatever makes your life and testing easier, works. Also, I’ll use an axiom related to testing here, “Test until fear turns into boredom”. You can apply the same to your MVP implementation. Testing and design patterns are here to eradicate your fear of failure, not to bore you.
So, in this guide, I’ll be using a use case of a QR Code Scanner Activity built using MVP pattern which we have employed in Open Event Orga Application (Github Repo). Because we are focusing on the test pattern and how to make it easy for us to test our app logic, I have omitted the model part from the MVP equation. The reason being that the possible model in the application would have been the camera loader or barcode initializer and the caveats associated with following this is that both these modules rely heavily on the Android specific view classes, namely, SurfaceView and other lifecycle methods. You could always create your way around it to include them in a separate model, but it won’t help us in writing unit tests for them, because firstly, they aren’t our logic to test, and secondly, they can’t be tested in a unit test (those models would have probably just implemented certain setters and getters).
So, the main purpose of our QR Code Scanner class is to scan a QR code and match it with a list of identifiers, and if there is a match, return it successfully to the caller. In this specific example, the identifiers will the ticket IDs of event attendees and the caller will be event organizer scanning QR codes to check the attendee in. The use case sounds simple, but has several mini use cases and dependencies of itself, which we have to take into our account while designing the View and Presenter class. Let’s discuss them one by one:
App State – Start: Activity starts, loads camera
- Permission Granted: detects that the app already has Camera Permission,
- Permission Absent: detects that the app doesn’t have Camera Permission,
- Denied: asks for it, denied, shows error
- Accepted: asks for it, granted, starts scanning
App State – QR Code Detected: Starts parsing them
- Attendees are not present: Attendees aren’t present because of some reason, either due to internal error or have not loaded yet
- Attendees are present:
- QR Code does not match with any attendee : Do nothing
- QR Code matches with one of the attendee : Send attendee to caller
App State: Camera or containing View is getting destroyed
There can be much more internal data flows, but this much is sufficient for our example. So, let’s start defining our contracts using the above knowledge. First, we will design our View. So what should be our strategy? Always think of presenters and views to be mapped in a 1:1 relation. They can have conversations with each other, the difference is that the view is only allowed to talk to the presenter, but presenter may talk to models too. So, the view is going to get all of its information from the presenter and can only take action when presenter tells it to, essentially saying that the view is dumb and passive. The presenter can talk to view and model(s), meaning the collection of information presenter has does not have to come from the view only. In fact, the less dependent presenter has to be on view, the better. The main motive of MVP is to make our logic less dependent on views.
Presenter Contract
In our example, presenter relies on the view to tell it when a certain event happens, they can be lifecycle callbacks, permission grants/denies, camera load/destroy or anything purely related to the Android implementation. So, we generally know from the start what kind of information presenter needs from a View, so let’s design our presenter first.
public interface IScanQRPresenter {
void attach(long eventId, IScanQRView scanQRView);
void start();
void detach();
void cameraPermissionGranted(boolean granted);
void onBarcodeDetected(Barcode barcode);
void onScanStarted();
void onCameraLoaded();
void onCameraDestroyed();
}
The methods are self-explanatory. Don’t worry if you didn’t get why we defined certain hooks like onScanStarted() or why we didn’t define onScanStopped(). These kinds of details will reveal themselves as you develop your components. You can skip to next section if you don’t want to know why we did it.
Basically, you should define a callback for events which are not reliable to be synchronized. What? Let me explain. Let’s say you got your camera permission and requested the view to start scanning, but you have to wait till the camera is loaded as it is not a synchronous call (and it shouldn’t be or your main thread will block). So, instead of that, our presenter will request the camera to load, go in an idle state, wait for the onCameraLoaded() call, and then request the scan to start, and since it is also not a synchronous work, it will go into idle state again and wait for onScanStarted() and then further its work. Whenever the camera is destroyed, the onCameraDestroyed() callback will be called and we will stop the scanning, and since there is nothing to be done after that, we won’t wait till scanning has stopped, thus dropping the need of onScanStopped() callback.
View Contract
View contract will come naturally to you once you have understood the data flow. It will contain the commands presenter will issue on the view and also, the requests for data that view holds. So, this will be our view.
public interface IScanQRView {
boolean hasCameraPermission();
void requestCameraPermission();
void showPermissionError(String error);
void onScannedAttendee(Attendee attendee);
void showBarcodePanel(boolean show);
void showBarcodeData(@NonNull String data);
void showProgressBar(boolean show);
void loadCamera();
void startScan();
void stopScan();
}
Almost all commands are straightforward but let me quickly explain showBarcodePanel(boolean) and showBarcodeData(String). They are used to display the currently visible barcode data to the user.
So, with our implementations set and data flow in place, let’s start writing tests for the feature. Yes, we’ll write tests without implementation and then you’ll see how easy it will be to write your views and presenters with only one goal to mind, to make the tests pass. If your tests are written correctly and cover everything, you should feel confident that your app will work without even seeing the actual implementation, because that is what tests are for. And by making passive and dumb views, imagine how light your instrumentation tests will be. Just check that the individual methods in view implementation are working as expected and you are done! No need to test logic or complex interactions, etc because you have got it covered in the unit tests themselves. This is not only a benefit of data flow tests but also a best practice. You always want to follow DRY, Don’t Repeat Yourself, even while testing.
Tests
So, we will start writing our tests now and you’ll realize how easy it is and all the hard work of abstraction and designing will pay off.
Attach Tests
So, firstly, we will test if the presenter calls appropriate methods when it is attached
@Test
public void shouldLoadAttendeesAutomatically() {
when(eventRepository.getAttendees(eventId, false))
.thenReturn(Observable.fromIterable(attendees));
scanQRPresenter.start();
verify(eventRepository).getAttendees(eventId, false);
}
@Test
public void shouldLoadCameraAutomatically() {
when(eventRepository.getAttendees(eventId, false))
.thenReturn(Observable.fromIterable(attendees));
scanQRPresenter.start();
verify(scanQRView).loadCamera();
}
Here, we are using Mockito to mock our EventDataRepository to return locally defined attendees instead of doing an actual network call. Then we are calling attach on presenter in each method and then we verify in the first test that the presenter is calling getAttendees on the EventDataRepository, and in the second test that it is requesting the view to load the camera.
Note that in the implementation of attach function, both loading of Camera and attendee loading will take place, but it is best practice to test them separately so that when a test fails, we know why it did
Detach Tests
@Test
public void shouldDetachViewOnStop() {
when(eventRepository.getAttendees(eventId, false))
.thenReturn(Observable.fromIterable(attendees));
scanQRPresenter.start();
assertNotNull(scanQRPresenter.getView());
scanQRPresenter.detach();
assertNull(scanQRPresenter.getView());
}
@Test
public void shouldNotAccessViewAfterDetach() {
scanQRPresenter.detach();
scanQRPresenter.start();
scanQRPresenter.onCameraLoaded();
scanQRPresenter.cameraPermissionGranted(false);
scanQRPresenter.cameraPermissionGranted(true);
scanQRPresenter.onScanStarted();
scanQRPresenter.onBarcodeDetected(barcode2);
scanQRPresenter.onCameraDestroyed();
verifyZeroInteractions(scanQRView);
}
In detach tests, we are verifying that after attaching and view not being null, the detach method call makes the presenter leave the reference to the view making it null. And in the second test, we do all possible interactions after detaching and confirm that no call whatsoever was made on the view. Tests like these enforce to avoid memory leaks and check for any NullPointerExceptions that may happen after the view was made null.
Note: This does not mean that memory leaks will not happen if this test passes. You can cause memory leaks by giving the view reference to any long living object, not just the presenter. This just ensures that presenter will not hold the view reference after detach and won’t reference to the null view in future.
Permission Tests
@Test
public void shouldStartScanOnCameraLoadedIfPermissionPresent() {
when(scanQRView.hasCameraPermission()).thenReturn(true);
scanQRPresenter.onCameraLoaded();
verify(scanQRView).startScan();
}
@Test
public void shouldAskPermissionOnCameraLoadedIfPermissionsAbsent() {
when(scanQRView.hasCameraPermission()).thenReturn(false);
scanQRPresenter.onCameraLoaded();
verify(scanQRView).requestCameraPermission();
}
@Test
public void shouldStartScanningOnPermissionGranted() {
scanQRPresenter.cameraPermissionGranted(true);
verify(scanQRView).startScan();
}
@Test
public void shouldShowErrorOnPermissionDenied() {
scanQRPresenter.cameraPermissionGranted(false);
verify(scanQRView).showPermissionError(matches("(.*permission.*denied.*)|(.*denied.*permission.*)"));
}
The permission tests are straightforward:
Implicit Permission Handling
- If the view already has the camera permission and camera has loaded, start scanning
- If the view does not have camera permission and the camera has loaded, request the permission
Request Handling
- If the request was granted, start scanning
- If the permission was denied, show the permission error. Here, I have used regex to match that the error message contains permission and denied words, you can use anyString() from Mockito for more flexibility or a specific message for more tight testing
Camera Destroy Test
@Test
public void shouldStopScanOnCameraDestroyed() {
scanQRPresenter.onCameraDestroyed();
verify(scanQRView).stopScan();
}
Pretty simple, stop the scan on destruction of camera
Flow Tests
You can also test that the callback flow happens in order so that not only the unit tests work but also the implementation logic is correct.
/**
* Checks that the flow of commands happen in order
*/
@Test
public void shouldFollowFlowOnImplicitPermissionGrant() {
when(eventRepository.getAttendees(eventId, false))
.thenReturn(Observable.fromIterable(attendees));
when(scanQRView.hasCameraPermission()).thenReturn(true);
scanQRPresenter.start();
InOrder inOrder = inOrder(scanQRView);
inOrder.verify(scanQRView).loadCamera();
scanQRPresenter.onCameraLoaded();
inOrder.verify(scanQRView).startScan();
}
@Test
public void shouldShowProgressInBetweenImplicitPermissionGrant() {
when(eventRepository.getAttendees(eventId, false))
.thenReturn(Observable.fromIterable(attendees));
when(scanQRView.hasCameraPermission()).thenReturn(true);
scanQRPresenter.start();
InOrder inOrder = inOrder(scanQRView);
inOrder.verify(scanQRView).showProgressBar(true);
scanQRPresenter.onCameraLoaded();
scanQRPresenter.onScanStarted();
inOrder.verify(scanQRView).showProgressBar(false);
}
Here, we are verifying two things, first that if we have the request granted implicitly, we load the camera and start the scan in order. This test isn’t that useful as we already tested that loading of the camera is done on attach and scan is started when the camera is loaded. In fact, this is an example of breaking the DRY rule. Even though it doesn’t hurt to include this, it also doesn’t help as it does not cover anything that hasn’t already been tested.
The second test is important and tests that progress bar is correctly shown and hidden after certain communications have taken place and the scan has started. Similarly, we can also test the progress bar behavior over all of the possible combinations of cases that can happen. The code snippets below show the tests:
@Test
public void shouldShowProgressInBetweenImplicitPermissionDenyRequestGrant() {
when(eventRepository.getAttendees(eventId, false))
.thenReturn(Observable.fromIterable(attendees));
when(scanQRView.hasCameraPermission()).thenReturn(false);
scanQRPresenter.start();
InOrder inOrder = inOrder(scanQRView);
inOrder.verify(scanQRView).showProgressBar(true);
scanQRPresenter.onCameraLoaded();
scanQRPresenter.cameraPermissionGranted(true);
scanQRPresenter.onScanStarted();
inOrder.verify(scanQRView).showProgressBar(false);
}
@Test
public void shouldShowProgressInBetweenImplicitPermissionDenyRequestDeny() {
when(eventRepository.getAttendees(eventId, false))
.thenReturn(Observable.fromIterable(attendees));
when(scanQRView.hasCameraPermission()).thenReturn(false);
scanQRPresenter.start();
InOrder inOrder = inOrder(scanQRView);
inOrder.verify(scanQRView).showProgressBar(true);
scanQRPresenter.onCameraLoaded();
scanQRPresenter.cameraPermissionGranted(false);
inOrder.verify(scanQRView).showProgressBar(false);
}
QR Code Detection Tests
@Test
public void shouldNotSendAnyBarcodeIfAttendeesAreNull() {
sendNullInterleaved();
verify(scanQRView, never()).onScannedAttendee(any(Attendee.class));
}
@Test
public void shouldNotSendAttendeeOnWrongBarcodeDetection() {
scanQRPresenter.setAttendees(attendees);
sendNullInterleaved();
verify(scanQRView, never()).onScannedAttendee(any(Attendee.class));
}
@Test
public void shouldSendAttendeeOnCorrectBarcodeDetection() {
// Somehow the setting in setUp is not working, a workaround till fix is found
RxJavaPlugins.setComputationSchedulerHandler(scheduler -> Schedulers.trampoline());
scanQRPresenter.setAttendees(attendees);
barcode1.displayValue = "test4-91";
scanQRPresenter.onBarcodeDetected(barcode1);
verify(scanQRView).onScannedAttendee(attendees.get(3));
}
Lastly, the core tests of our presenter. To explain the tests, let me show you the sendNullInterleaved() method
private void sendNullInterleaved() {
sendBarcodeBurst(barcode1);
sendBarcodeBurst(null);
sendBarcodeBurst(barcode1);
sendBarcodeBurst(null);
sendBarcodeBurst(barcode2);
sendBarcodeBurst(barcode2);
sendBarcodeBurst(null);
sendBarcodeBurst(barcode1);
sendBarcodeBurst(null);
}
So what it basically does is send some barcodes interleaved with null (no barcode detected) to the presenter using the onBarcodeDetected method to emulate the real camera sending barcode values with sometimes sending null whenever no barcode is in view.
The first test simply checks that no attendee is sent if the attendee list is null. Seems pretty obvious. The second one checks that if barcode does not match with any attendee’s identifier, it should not send any attendee as well. It does this by setting an arbitrary list of attendees with different identifiers and sending non-matching barcodes to the presenter. Lastly, the successful test, where a single matching barcode is sent to the presenter and it should send that particular attendee with matching identifier.
Phew! Quite a lot of tests and we are done. The tests were not very large and mostly self-explanatory. Now, you just have to implement the view and presenter methods till all the tests light up to be green and you are done! You have created a feature using MVP design and implemented it using test driven development.
So start testing and get a seal of reliability and confidence about your code and the satisfaction of seeing the green bar fill up.
You must be logged in to post a comment.