How We Develop Mobile Applications - Pt 4 - Android

Unit testing out MVP architecture

Previously we kicked off creating our model. Now, we’re ready to start setting up the presenter part of our testable Model-View-Presenter architecture.

Getting started with MVP

By working with Model-View-Presenter, we are able to abstract away much of our user interface implementation and test the underlying functionality using unit tests. Instrumentation tests are great, and we’ll add those too, but those take longer to run. If we can push most of our tests into quick, direct unit tests, we are likely to run our tests more often. We will also receive feedback much more quickly if something goes wrong.

We will start by using TDD to create our view interface and presenter. By using an interface for the view, we are able to provide a mock to the presenter in our test that is independent of the Android framework. This allows us to focus on the language of the behavior rather than the implementation within the framework. This also has the added bonus of keeping the tests fast and lightweight. We can test that the presenter performs some action and prompts the view to display something, without caring about how it’s displayed.

Our presenter will have methods onAttach() and onRefresh(). We can also add methods for other lifecycle events and user actions as needed.

The view will be an interface defining methods such as showLoading(loading: Bool), showNoUsers(), and showUsers(List<User>). How a view instance implements these methods does not matter to us at this point in time. Typically, we will have an Activity that implements one or more view interfaces, but we can deal with that later.

Building our presenter test

Of course, we’ll start by writing our test. Since we only want to verify how the presenter interacts with the view and we don’t care how the view is implemented, we are going to use Mockito-Kotlin to mock it. Add the dependency to the gradle file so we can get to work with it.

testCompile "com.nhaarman:mockito-kotlin:1.4.0"

Let’s start by verifying that when the onAttach() method is called on the presenter, the view is told to start showing the loading view. To do this, we create a mock of our view interface and an instance of the presenter. Then we call onAttach() on the presenter, and verify showLoading(true) was called on the view.

class ClaimsPresenterTest {
  @Test
  fun onAttach_showsLoading() {
    val view = mock<ClaimsPresenter.View>()
    val presenter = ClaimsPresenter()

    presenter.onAttach()

    verify(view).showLoading(true)
  }
}

We will write just enough code to make it compile. This means a presenter class with an empty onAttach() method, and a view interface with showLoading().

class ClaimsPresenter() {
  interface View {
    fun showLoading(isLoading: Boolean)
  }

  fun onAttach() {}
}

Don’t get carried away trying to fill it out yet. Run the test, and watch it fail so we know our test is working. If we only ever see our test green, we don’t know that it’s actually testing anything.

Now that we have our failing test, we can make it green. The presenter needs a reference to the view, so we’ll pass it in.

class ClaimsPresenterTest {
  @Test
  fun onAttach_showsLoading() {
    val view = mock<ClaimsPresenter.View>()

    val presenter = ClaimsPresenter(view)

    presenter.onAttach()

    verify(view).showLoading(true)
  }
}

Run the tests again, and they should still fail. We need to actually call view.showLoading(true) in onAttach():

class ClaimsPresenter(private val view: View) {
  interface View {
    fun showLoading(isLoading: Boolean)
  }

  fun onAttach() {
    view.showLoading(true)
  }
}

And now our test is green!

We’ll follow the same pattern to make sure that later, the presenter also calls showLoading(false). By using inOrder we can verify the order in which these calls are made.

class ClaimsPresenterTest {
  @Test
  fun onAttach_showsLoading() {
    val view = mock<ClaimsPresenter.View>()

    val presenter = ClaimsPresenter(view)

    presenter.onAttach()

    val inOrder = inOrder(view)
    inOrder.verify(view).showLoading(true)
    inOrder.verify(view).showLoading(false)
  }
}

Update the code to make the tests pass:

class ClaimsPresenter(private val view: View) {
  interface View {
    fun showLoading(isLoading: Boolean)
  }

  fun onAttach() {
    view.showLoading(true)
    view.showLoading(false)
  }
}

Who’s bringing donuts?

This all is great, but what good is it if we don’t know who’s bringing donuts? Let’s add a mock API to the mix, and do the same thing to make sure it’s being called between the view being set to loading and not loading. We’ll start with an easy case: no one is bringing donuts. We want to make sure the API is called, and showNoUsers() is called.

We write our test, watch it fail, then write the code to make it pass.

class ClaimsPresenterTest {
  @Test
  fun onAttach_togglesLoadingWhileFetchingClaimsFromServer() {
    val view = mock<ClaimsPresenter.View>()

    val api = mock<DonutsApi>()
    whenever(api.getTodayClaims()).thenReturn(Observable.just(listOf()))

    val presenter = ClaimsPresenter(view, api)

    presenter.onAttach()

    val inOrder = inOrder(view, api)
    inOrder.verify(view).showLoading(true)
    inOrder.verify(api).getTodayClaims()
    inOrder.verify(view).showLoading(false)
    inOrder.verify(view).showNoUsers()
  }
}

Note that in order to check ordered calls on multiple mocks, we pass in each mock that we want to verify when we create the inOrder object.

Now we need to update the presenter’s constructor to take the DonutsApi instance in order to compile. Run the tests and see it fail. Next update the onAttach() method to call the API while the view is loading, and update the view using showNoUsers() when everything is done.

class ClaimsPresenter(private val view: View, private val api: DonutsApi) {
  interface View {
    fun showLoading(isLoading: Boolean)
    fun showNoUsers()
  }

  fun onAttach() {
    view.showLoading(true)
    api.getTodayClaims()
    view.showLoading(false)
    view.showNoUsers()
  }
}

Now, let’s get some donuts! Write a new test and queue up some users in our mock server. We’ll check the same flow as the previous test, except now we want showUsers(List<User>) to be called instead of showNoUsers().

class ClaimsPresenterTest {
  // ...

  @Test
  fun onAttach_showsUsersFromServer() {
    val view = mock<ClaimsPresenter.View>()

    val user = User(
        id = "id",
        name = "Josh Kovach",
        displayName = "Josh",
        githubLogin = "shekibobo"
    )
    val api = mock<DonutsApi>()
    whenever(api.getTodayClaims()).thenReturn(Observable.just(listOf(user)))

    val presenter = ClaimsPresenter(view, api)

    presenter.onAttach()

    val inOrder = inOrder(view, api)
    inOrder.verify(view).showLoading(true)
    inOrder.verify(api).getTodayClaims()
    inOrder.verify(view).showLoading(false)
    inOrder.verify(view).showUsers(listOf(user))
  }
}

Here’s where it will get a bit trickier. While the case with no users was simple, it didn’t get us all the way there. We are returning Observables in our API calls, and we need subscribe to the Observable to actually perform the network request and return our user list. This is one reason to make sure we cover a variety of scenarios in our unit tests. Let’s make the modifications needed to make our test pass.

class ClaimsPresenter(private val view: View, private val api: DonutsApi) {
  interface View {
    fun showLoading(isLoading: Boolean)
    fun showNoUsers()
    fun showUsers(users: List<User>)
  }

  fun onAttach() {
    view.showLoading(true)
    api.getTodayClaims()
        .subscribe({ users ->
          view.showLoading(false)
          if (users.isEmpty()) {
            view.showNoUsers()
          } else {
            view.showUsers(users)
          }
        })
  }
}

Now we have ensured that our view will be updated with the correct results from our API, and our loading screen will show and hide while we’re waiting. We can now drop this into an Activity, Fragment, or mock it out in other tests, and we won’t have to duplicate the logic for fetching users. We can continue to add test coverage for error handling, caching, network retries, and other edge cases without ever needing to load the Android framework. Take advantage of the standalone class to carve out the details of how a view and presenter may interact as your application evolves.

It’s important to note that if you hook this sample presenter up to an Activity, you need to make sure that any updates to the UI are performed on the main thread.

Conclusion

Now we know how we can use tests to guide the development of our view and presenter. By starting with tests, we ensure we keep as much of our code testable as possible. It keeps us on track with single responsibility classes, and prevents regressions by having good, reliable, and fast tests from the start.

As our application grows and changes, we will be able to make changes to the user interface without having to update the code that performs the business logic. We can use multiple presenters on a single Activity to prevent Activities from getting bloated with too much responsibility. With MVP, we can keep our Activities small, and focused solely on displaying the information to the user.


To view other posts in this blog series broken down by OS, see below:

Both

Part 1 - How We Develop Mobile Applications - Best Practices

Android

Part 2 - Getting Our Environment Set Up for Android

Part 3 - Using TDD to build our models and API client

iOS

Part 2 - Getting Our Environment Set Up for iOS

Part 3 - Building a Cheap Prototype to Validate Design

Photo of Victoria Gonda

Victoria is a software developer working on mobile and full stack web applications. She enjoys exchanging knowledge through conference talks and writing.

Photo of Joshua Kovach

Josh’s skills include web and mobile development and he enjoys developing APIs and Android Apps. He is also a mentor on HackHands, pairing with programmers working through coding issues.

Comments:


Post a Comment

(optional)
(optional — will be included as a link.)