21 February 2019
How to TDD your MVP? Your friend’s quick guide to testing & MVP architecture
Everyone knows just how important testing in software development is… wait! Everyone? No, apparently there are still some devs out there who don’t appreciate writing tests all that much and have absolutely no intention to get familiar with test-driven development! We know that you’re not one of them. You’re cool. But if you happen to have friends like this, please, refer them to our introduction to TDD in the Model-View-Presenter pattern.
Make them learn why tests are crucial, why they should be written during development, rather than post factum, and why MVP is simply made for TDD.
Let’s start our tour de test with a quick recap of why making tests a part of your development process makes so much sense.
Software testing – why is it so important?
Testing is a crucial part of developing software… Well, it should be at least… And yet – sadly – it is still not a standard practice.
And that’s all despite a myriad of reasons why you should definitely incorporate tests into your development process:
- Tests boost your confidence.
The fact that there is any test coverage says that you have spent time working on the app reliability, rather than just clicked through all happy paths.
- Tests save (tons of) time.
If you’re testing a particular feature (regardless of platform), you probably need to go through some steps to test it. So – you write your code, build your app, test your changes. If you are lucky enough and everything is OK, you move to another task… If not (and let’s face it – it usually isn’t), then you alter you code, build your app…. see where this is going?
The result of testing little chunks of code is the fact that by the time you check if the whole feature works correctly (which will be pretty much the last step), you can be quite sure that everything you’ve build so far meets acceptance criteria.
- Tests make it easier to add new features.
Suppose you wrote your feature and now you need to add a new one – how can you tell what impact your changes will have on your existing code? Even worse – what if someone else wrote the feature?
- Tests make it easier to refactor legacy code.
Software ages, technologies change, business models change, requirements change – there’s no way to successfully maintain a long-lasting and evolving project if you don’t optimize and maintain your code. But how can you have confidence that the changes in your codebase will not introduce new bugs?
- Tests make for cleaner code.
Tests impose code readability and encapsulation as well as encourage the use of the single responsibility principle. There is a good chance that if you have unit tests for a particular method, it is written in a clear way….and if it isn’t, you can refactor it! At the very least, you’ll know that you haven’t broken anything.
Model-View-Presenter & testing in mobile development
Despite the fact that the first iPhone was introduced almost 12 years ago, mobile software development is still a young branch of software development. And as any new technology, it comes with new and unique challenges. Let’s take a closer look at them through the lens of the popular Model-View-Presenter architecture.
Back in the day, developers came up with the Model-View-Controller architectural pattern to maintain separation of concerns in the code. While frameworks like Symfony, Spring and Ruby on Rails thrive on MVC, the pattern is not all that perfect when it comes to the development and testing of rich user interface apps.
We can observe a similar pattern on mobile platforms (Android and iOS)–controllers become heavily mixed with the View layer and over time, instead of what’s supposed to be an MVC, we end up with MassiveViewController – a monster of a class with tons of methods and responsibilities.
Model-View-Presenter is an iteration of MVC. The change might seem slight, but the difference in approach is significant.
Basically, the View initiates actions, which then are fetched by the Presenter to the Model. Subsequently, the Model provides resulting data to the Presenter, which uses it to update the View. Simple enough? It might seem so, but as they say: the devil’s in the details. Let’s take a closer look at all three.
Model
This is where all your business logic is. You store products downloaded from the API in your database? Do you send your payment requests? This is the place (layer) to put them. The Model layer should contain all the necessary logic for obtaining data.
View
The view is the only place that should touch your UI references directly. On Android, it can be an Activity or Fragment. It should not contain any logic whatsoever.
Presenter
It tells your View WHAT to show and WHEN to show it – in other words, it orchestrates your View. It means that the Presenter should not know HOW to show it – that’s the View’s concern. The Presenter should not contain any references to the Platform Framework or SDK.
Benefits
- UI logic is written purely in Java/Kotlin.
- It’s testable (easy mocking).
- Does a lot of heavy lifting for Activities/Fragments/ViewController.
What about unit testing?
Unit Testing in Model-View-Presenter
The sole purpose of using the MVP pattern is to be able to unit-test your View’s behaviour without having an UI Framework standing in the way.
When you don’t have to worry about mocking your (Android) dependencies (views, dialogs etc.), you can focus on testing how the UI should behave in different states.
Do not (no matter what!) pass any Android SDK dependencies to the Presenter (android.view.View, Context). They are usually hard to mock and you’ll probably need Robolectric to mimic framework classes. You should use a mocking framework such as Mockito for mimicking interface and class behaviour.
Here are some examples:
Hide everything behind an interface — it makes testing and mocking a no-brainer.
Don’t:
Do:
Don’t define a getter method on your View interface (getEmail(), getTime())
If you’re using Mosby or any popular Clean Architecture templates, you’ll see that the View is not attached in the after-instantiation of the Presenter. You’ll have to do a lot of null checks and provide default values.
Don’t:
Do:
Furthermore, having a lot of getters makes writing unit tests harder, as you have to create complex mocks:
Better:
MVP-driven Test-Driven Development
Test-Driven-Development is not a new concept. It has been a part of software development for years, but somehow it still is not that popular when it comes to developing android apps. Is it because mobile is still relatively young?
Just to recap, the concept is very simple – you first write your tests and then work on your implementation until all tests pass. In a nutshell, the process is as follows:
- Write your tests.
- Make your code compile.
- Run your tests.
- If tests fail, fix implementation and go to step 2, else go to step 4.
- Refactor.
- Done!
What do you get out of this?
- Tests are part of the development process.
- You can focus on WHAT the code should look like, not on HOW it should be implemented.
- Tests are actually code documentation.
- Test coverage for free!
Easy to say, isn’t it? So how to start TDDing?
Let’s say you have an app with a list of contacts and no documentation or specification. Here is some information that you know:
It’s a typical contact list app that fetches your contacts from the backend and displays an error message if any error is returned.
Your MVP looks like this:
How to start writing your tests? You’ll need test scenarios. I recommend using Gherkin-like notation. Here’s how it usually looks:
Scenario #1
// given
// define your initial conditions and assumptions
// when
// initiate action on component
// then
// put your assertions here
Let’s go back to our contact list and write some test scenarios. If you don’t know where to begin – start with a “happy path”
How does it translate to real code? More or less like this:
Notice that we have named the test as a normal sequence. To maintain readability in our build console, we keep //given//when//then comments to separate assumptions, actions and assertions. We also use inOrder to verify the order of calls. Why? Just a reminder:
<quote>Presenter decides WHEN and WHAT to show</quote>
Also verifyNoMoreInteractions() is important here as we have to ensure that no additional actions on the Viewshould are triggered.
Now, we can start writing an actual implementation. The first step is making sure that we are able to compile our code:
Contact – our model, which contains information
ContactsListApi – represents our API interface (e.g. Retrofit)
ContactsListContract – our MVP contract
ContactsListPresenter – implementation of our Presenter with View and Api interfaces implemented
Try to run the test. It fails, isn’t it? Let’s keep writing until the test passes.
You should end up with something like this:
Keep in mind that I’m deliberately not using threading just to make things as simple as possible. If you are executing code on different threads, you should probably use RxAndroidPlugin to run all the code on the default thread. There are already a great deal of excellent articles on how to do it.
And so, we have our happy path covered. But what if something goes wrong? What if our API call returns an error? Let’s add a test case for it. We start off with a user-story-like description:
Now, the actual test case:
Let’s rewrite our code to pass all the tests:
A point to note is that we are using the simplest way to handle an exception. In a real project, your API layer should probably return custom errors to make error handling more rigid:
Please, check out my demo project to see more examples of test cases and the full project setup.
MVP-style TDD – summary
We’ve barely scratched the surface of the topic of testing and test-driven development, but I hope that I’ve at least managed to show you the significance of testing your software. A few points to remember:
- The Model-View-Presenter pattern makes for a great opportunity to really get into test-driven development.
- Testing software is every developer’s responsibility.
- Tests really do save time.
- Don’t mock what you don’t own, a.k.a. don’t test the framework.
- Make writing tests an integral part of your development process, not the result.
Above all, though: