Chapters

Hide chapters

Advanced Android App Architecture

First Edition · Android 9 · Kotlin 1.3 · Android Studio 3.2

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

9. Testing MVP
Written by Yun Cheng

Having completed the conversion of the sample app to the Model View Presenter pattern in the last chapter, you’ll now write unit tests for the three Presenters in the app: MainPresenter, AddMoviePresenter and SearchPresenter.

Getting started

Before you can write your tests, there are some housekeeping steps you need to complete:

  1. Create a base test class to wrap the capture() functionality for Mockito ArgumentCaptors.
  2. Create a custom TestRule for testing your RxJava calls.

Getting to know Mockito

This book will, for the most part, will use Mockito to help with testing. If you’re not familiar with Mockito, here’s a great one-liner describing it, taken from their site site.mockito.org

The Mockito library enables mock creation, verification and stubbing.

If you want a deep dive into the library, its highly recommended to check out the tutorials on the Ray Wenderlich site, specifically Android Unit Testing with Mockito www.raywenderlich.com/195-android-unit-testing-with-mockito.

However, a high level description of some of the features we’ll be using is given below:

  • mock()/@Mock: create mock optionally specify how it should behave via Answer/MockSettings
  • when()/given() to specify how a mock should behave If the provided answers don’t fit your needs, write one yourself extending the Answer interface
  • @InjectMocks: automatically inject mock fields annotated with @Mock
  • verify(): to check methods were called with given arguments can use flexible argument matching, for example any expression via the any() or capture what arguments were called using @Captor instead

The starter project should already have Mockito added as a dependency, however if you’re following along in your own project, be sure to add the following libraries to the dependencies in your build.gradle file.

testImplementation 'org.mockito:mockito-core:2.2.5'
testImplementation('com.nhaarman:mockito-kotlin-kt1.1:1.5.0', {
		exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
})

The first dependency pull Mockito in for unit testing, and the second dependency simply provides some helper functions to work with Mockito and Kotlin.

Mockito can be really helpful when writing unit tests, however, there’s more boiler plate stuff you’ll go over in the next section in order to get Mockito to work with Kotlin.

Wrapping Mockito ArgumentCaptors

Sometimes, the mock objects in your unit tests will make use of Mockito ArgumentCaptors in method arguments to probe into the arguments that were passed into a method. Using Mockito’s capture() method to capture an ArgumentCaptor is fine in Java, but when you write your unit tests in Kotlin, you’ll get the following error:

java.lang.IllegalStateException: classCaptor.capture() must not be null

This error is due to a difference in the way Java and Kotlin handle null safety. With Mockito’s ArgumentCaptor, capture() returns null. If the method you’re stubbing is expecting a non-null parameter, Kotlin enforces that rule and throws an error when you try to use an ArgumentCaptor.

If you still want the ability to use this feature in your Kotlin code, you must write a wrapper for ArgumentCaptor’s capture() method in a class, and then have your test classes extend that base class.

Inside test, create a new file named BaseTest.kt and add the following:

open class BaseTest {  
  open fun <T> captureArg(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
}

This BaseTest class contains the method captureArg(), making use of Generics to cast the null object returned by Mockito into the specific class object. Your test classes will extend BaseTest and have access to this modified version of capture() when you want to use them.

Adding a TestRule for RxJava Schedulers

Recall that by design, the Presenters in an MVP app do not have references to Android framework specific classes such as Context. This rule is what allows you to write JUnit tests on the Presenters. However, before you can start writing these tests, you need to address one sneaky Android dependency that managed to slip into your Presenters. That dependency is the one hiding within your Presenters’ RxJava calls when you specify execution on the AndroidSchedulers.mainThread().

If you try to write unit tests for the various RxJava calls you have in the Presenters, you’ll see this exception thrown:

Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.

When retrieving movies from the local database and getting search results from your web service, the observables produce results on the Schedulers.io() thread to avoid blocking the main thread, and the results are observed on the AndroidSchedulers.mainThread().

To fix this, you’ll need to tell your test code to use different schedulers from the ones in the production code you’re testing. You can achieve this using RxAndroidPlugins hooks and apply to your tests via a custom TestRule.

Note: A TestRule is an alteration in how a test method, or set of test methods, is run and reported. A TestRule may add additional checks that cause a test that would otherwise fail to pass, or it may perform necessary setup or cleanup for tests. TestRules can do everything that could be done previously with methods annotated with @Before or @After, but they are more powerful, and more easily shared between projects and classes. Multiple TestRules can be applied to a test or suite execution.

Inside test, create a new file named RxImmediateSchedulerRule.kt and add the following:

class RxImmediateSchedulerRule : TestRule {  

  override fun apply(base: Statement, d: Description): Statement {  
    return object : Statement() {  
      @Throws(Throwable::class)  
      override fun evaluate() {  
        //1
        RxJavaPlugins.setIoSchedulerHandler {
          Schedulers.trampoline()
        }  
        RxJavaPlugins.setComputationSchedulerHandler {
          Schedulers.trampoline()
        }  
        RxJavaPlugins.setNewThreadSchedulerHandler {
          Schedulers.trampoline()
        }  
        RxAndroidPlugins.setInitMainThreadSchedulerHandler {
          Schedulers.trampoline()
        }  

        try {
          //2  
          base.evaluate()  
        } finally {  
          //3
          RxJavaPlugins.reset()  
          RxAndroidPlugins.reset()  
        }  
      }  
    }  
  }  
}

Diving into the code:

  1. TestRule, when applied to tests, specifies the Schedulers.trampoline() scheduler to your test code, overriding whatever scheduler was specified by the production code.
  2. Call base.evaluate() to evaluate the statement.
  3. Reset the hook afterward.

Testing the MainPresenter

Now you’re ready to create your test classes, starting with the test class for MainPresenter. Because MainPresenter.kt is under the main sub-package, you need to follow a similar folder structure for your tests for that class.

Inside test, create a main sub-package. Then, in that sub-package, create a new file named MainPresenterTests.kt and add the following:

@RunWith(MockitoJUnitRunner::class)  
class MainPresenterTests : BaseTest() {  
  @Rule @JvmField var testSchedulerRule = RxImmediateSchedulerRule()  
}

This code sets up some rules for MainPresenterTests, which is a subclass of BaseTest. Having it run with MockitoJUnitRunner will validate framework usage after each test method and initialize mocks annotated with @Mock, while adding the RxImmediateSchedulerRule will address the RxJava Schedulers issue as explained in the previous section.

Next, you’ll set up the instances of the View, the Model and the Presenter. The Presenter is the class you’re testing, while the View and the Model are dependencies that you can mock using the @Mock annotation.

Continue by adding the following code to MainPresenterTests:

@Mock  
private lateinit var mockActivity : MainContract.ViewInterface  

@Mock  
private lateinit var mockDataSource : LocalDataSource  

lateinit var mainPresenter : MainPresenter

You’ll link these components together in a setUp() method that runs before every test, as denoted by the @Before annotation:

@Before  
fun setUp() {  
  mainPresenter = MainPresenter(viewInterface = mockActivity, dataSource = mockDataSource)  
}

Here, the MainPresenter is instantiated, passing in the mock objects for the View and Model.

Now that the basic test infrastructure is ready to go, it’s time to start writing some tests.

Testing movie retrieval

The first tests you’ll write for the MainPresenter verifies the getMyMoviesList() method. Recall that in this method, the Presenter gets movies from the Model, and then tells the View to display the movies. To facilitate the testing of this method, create a dummy list of movies:

private val dummyAllMovies: ArrayList<Movie>  
get() {  
  val dummyMovieList = ArrayList<Movie>()  
  dummyMovieList.add(Movie("Title1", "ReleaseDate1", "PosterPath1"))  
  dummyMovieList.add(Movie("Title2", "ReleaseDate2", "PosterPath2"))  
  dummyMovieList.add(Movie("Title3", "ReleaseDate3", "PosterPath3"))  
  dummyMovieList.add(Movie("Title4", "ReleaseDate4", "PosterPath4"))  
  return dummyMovieList  
}

Next, add this test for getting a non-empty list of movies:

@Test
fun testGetMyMoviesList() {  
  //1
  val myDummyMovies = dummyAllMovies  
  Mockito.doReturn(Observable.just(myDummyMovies)).`when`(mockDataSource).allMovies

  //2  
  mainPresenter.getMyMoviesList()  

  //3
  Mockito.verify(mockDataSource).allMovies  
  Mockito.verify(mockActivity).displayMovies(myDummyMovies)
}

Reviewing the code step-by-step:

  1. Set up the test by stubbing the method mockDataSource.allMovies to return the dummyAllMovies list of movies you created, instead of the default data source behavior that would hit the database.

  2. Invoke the getMyMoviesList() method that you’re testing.

  3. Verify that the Presenter calls on the Model to get the movies and calls on the View to display the movies.

Run the test by right-clicking the MainPresenterTests.kt tab and clicking Run “MainPresenterTests”. Confirm that it passes. If so, you just wrote your first passing test — but don’t spend too much time basking in the green glory, you have more tests to write.

To continue, add this test for getting an empty list of movies:

@Test  
fun testGetMyMoviesListWithNoMovies() {  
  //1
  Mockito.doReturn(Observable.just(ArrayList<Movie>())).`when`(mockDataSource).allMovies  

  //2
  mainPresenter.getMyMoviesList()  

  //3
  Mockito.verify(mockDataSource).allMovies  
  Mockito.verify(mockActivity).displayNoMovies()  
}

Taking this code in turn:

  1. Set up the test by stubbing the method mockDataSource.allMovies to return the empty list of movies, again, to override the default data source behavior of accessing the database.
  2. Invoke the getMyMoviesList() method that is under test.
  3. Verify that the Presenter calls on the Model to get the movies and calls on the View handle the displaying of no movies.

Run the tests again, and make sure you still see green.

Congratulations! You can now feel confident that your activity will call use the model to retrieve movies, and will pass the results on to the view.

Testing deleting movies

Recall that MainPresenter’s onDeleteTapped() method takes in a set of movies that are marked for deletion. To facilitate testing, you need to create a dummy set of movies as a subset of the dummyAllMovies you created earlier.

Add the following code to MainPresenterTests, just below the other class properties:

private val deletedHashSetSingle: HashSet<Movie>  
  get() {  
    val deletedHashSet = HashSet<Movie>()  
    deletedHashSet.add(dummyAllMovies[2])  

    return deletedHashSet  
  }  

private val deletedHashSetMultiple: HashSet<Movie>  
  get() {  
    val deletedHashSet = HashSet<Movie>()  
    deletedHashSet.add(dummyAllMovies[1])  
    deletedHashSet.add(dummyAllMovies[3])  

    return deletedHashSet  
  }

You’ll use these static lists in the tests you’re about to write.

In your tests, you’ll verify that the Presenter calls on the Model to delete the right movies and then calls on the View to display a message upon finishing the deletion.

Add the following code for testing deleting a single movie:

@Test  
fun testDeleteSingle() {  

  //1
  val myDeletedHashSet = deletedHashSetSingle  
  mainPresenter.onDeleteTapped(myDeletedHashSet)  

  //2
  for (movie in myDeletedHashSet) {  
    Mockito.verify(mockDataSource).delete(movie)  
  }  

  //3
  Mockito.verify(mockActivity).showToast("Movie deleted")  
}

Here’s the code breakdown:

  1. Invoke onDeleteTapped, passing in the HashSet of movies for the Presenter to delete.
  2. Iterate through the set of movies and verify that they’ve been deleted by checking that the appropriate calls were executed against the mock data source.
  3. Verify that the correct Toast message is displayed for a single movie deleted.

Run this test and confirm it passes. Then, add the test for deleting multiple movies, which looks similar:

@Test  
fun testDeleteMultiple() {  

  //Invoke  
  val myDeletedHashSet = deletedHashSetMultiple  
  mainPresenter.onDeleteTapped(myDeletedHashSet)  

  //Assert  
  for (movie in myDeletedHashSet) {  
    Mockito.verify(mockDataSource).delete(movie)  
  }  

  Mockito.verify(mockActivity).showToast("Movies deleted")  
}

Run the tests again and make sure everything passes before moving on to the next section.

Testing the AddMoviePresenter

Next, you’ll write tests for AddMoviePresenter. Because AddMoviePresenter.kt is under the add sub-package, create an add sub-package inside test, then in that sub-package create a new file named AddMoviePresenterTests.kt. Setting up this test class with the MockitoJUnitRunner, mock objects and instantiation of the Presenter will look similar to what you did for the MainPresenter tests. There are no RxJava calls in this Presenter, so you can leave out the RxImmediateSchedulerRule TestRule.

Start by adding the following code to AddMoviePresenterTests.kt:

//1
@RunWith(MockitoJUnitRunner::class)  
class AddMoviePresenterTests : BaseTest() {  

  //2
  @Mock  
  private lateinit var mockActivity : AddMovieContract.ViewInterface  

  @Mock  
  private lateinit var mockDataSource : LocalDataSource  

  lateinit var addMoviePresenter : AddMoviePresenter  

  @Before  
  fun setUp() {  
    //3
    addMoviePresenter = AddMoviePresenter(viewInterface = mockActivity, dataSource = mockDataSource)  
  }
}

Walking through the code, you:

  1. Annotate the test class to run with MockitoJUnitRunner to specify that the test should use the mock test runner as opposed to the standard JUnit runner.
  2. Initialize the class properties, including mock objects for the View and Model.
  3. Inject the mock View and mock Model into the Presenter’s constructor when instantiating the addMoviePresenter.

Testing adding movies

Recall that at a minimum, the user must enter a movie title to add a movie to their to-watch list. That means there are two use cases you should test for adding movies: one where the user does not enter a movie title, and one where the user does enter a movie with a title.

In AddMoviePresenterTests.kt, add the following for the first test:

@Test  
fun testAddMovieNoTitle() {  
  //1
  addMoviePresenter.addMovie("", "", "")  

  //2
  Mockito.verify(mockActivity).displayError("Movie title cannot be empty")  
}

Taking the code in turn, you:

  1. Invoke the Presenter’s addMovie() method, passing in empty strings for the movie’s title, release date and poster path.
  2. Verify that the View will display an error in this situation.

Run this test and confirm that it passes.

Now, add the second test:

//1  
@Captor  
private lateinit var movieArgumentCaptor: ArgumentCaptor<Movie>

@Test  
fun testAddMovieWithTitle() {  

  //2
  addMoviePresenter.addMovie("The Lion King", "1994-05-07", "/bKPtXn9n4M4s8vvZrbw40mYsefB.jpg")  

  //3  
  Mockito.verify(mockDataSource).insert(captureArg(movieArgumentCaptor))  
  //4
  assertEquals("The Lion King", movieArgumentCaptor.value.title)  

  //5
  Mockito.verify(mockActivity).returnToMain()  
}

This is similar to the previous test, with some extras. Here’s the breakdown:

  1. Create an ArgumentCaptor that captures the movie object passed into the Model’s insert() method.
  2. Invoke the Presenter’s addMovie() method, passing in proper values for a movie’s title, release date and poster path.
  3. Verify that the Presenter asks the Model to insert the movie into the database. Meanwhile, you use BaseTest’s custom captureArg() method to capture the movie object that was passed into the Model.
  4. Verify using the ArgumentCaptor the nature of the movie instance that was passed into the Model’s insert() method. In this case, you confirm that the title matches what the Presenter received.
  5. Verify that the Presenter asks the View to navigate back to the main screen afterward.

Run this test and confirm that it passes.

Testing the SearchPresenter

You’ll wrap up this chapter by creating some tests for SearchPresenter.

Create a new sub-package named search inside test. Then, create a file named SearchPresenterTests.kt and add the following code:

@RunWith(MockitoJUnitRunner::class)  
class SearchPresenterTests : BaseTest() {  
  @Rule  
  @JvmField var testSchedulerRule = RxImmediateSchedulerRule()  

  @Mock  
  private lateinit var mockActivity : SearchContract.ViewInterface  

  @Mock  
  private val mockDataSource = RemoteDataSource()  

  lateinit var searchPresenter: SearchPresenter  

  @Before  
  fun setUp() {  
    searchPresenter = SearchPresenter(viewInterface = mockActivity, dataSource = mockDataSource)  
  }
}

The code is nearly the same as it was for the previous test classes, so a belabored explanation isn’t needed here.

To test SearchPresenter’s searching functionality, you’ll test two use cases: when the TMDB API call successfully returns a list of movies, and when an error occurs with the TMDB response. Rather than making the web call, you’ll stub the response returned with a dummy list of movies.

Add the following helper code:

private val dummyResponse: TmdbResponse  
  get() {  
    val dummyMovieList = ArrayList<Movie>()  
    dummyMovieList.add(Movie("Title1", "ReleaseDate1", "PosterPath1"))  
    dummyMovieList.add(Movie("Title2", "ReleaseDate2", "PosterPath2"))  
    dummyMovieList.add(Movie("Title3", "ReleaseDate3", "PosterPath3"))  
    dummyMovieList.add(Movie("Title4", "ReleaseDate4", "PosterPath4"))  

    return TmdbResponse(1, 4, 5, dummyMovieList)  
  }

Next, add the test for a successful API call:

@Test  
fun testSearchMovie() {  
  //1
  val myDummyResponse = dummyResponse    Mockito.doReturn(Observable.just(myDummyResponse)).`when`(mockDataSource).searchResultsObservable(anyString())

  //2  
  searchPresenter.getSearchResults("The Lion King")

  //3
  Mockito.verify(mockActivity).displayResult(myDummyResponse)
}

Taking this code step-by-step:

  1. Set up the test by having the Model return the dummyResponse as its response from the API call.
  2. Invoke the Presenter’s getSearchResults, passing in the search query.
  3. Verify that because the Model returned a valid response, the Presenter then asks the View to display the results.

Run this test and confirm that it throws no errors.

Now, test the use case where the API call fails and throws an error by adding the following:

@Test  
fun testSearchMovieError() {  
  //1
  Mockito.doReturn(Observable.error<Throwable>(Throwable("Something went wrong"))).`when`(mockDataSource).searchResultsObservable(anyString())  

  //2
  searchPresenter.getSearchResults("The Lion King")  

  //3
  Mockito.verify(mockActivity).displayError("Error fetching Movie Data")  
}

There are a few new things in this code:

  1. Set up the test by having the Model return an error with the message “Something went wrong” instead of a proper response from the API call.
  2. Invoke the Presenter’s getSearchResults method, passing in the search query.
  3. Verify that upon receiving this error the Presenter asks the View to display an appropriate error message.

Run this test. It passes, but it throws an error in the output. Don’t worry; this is expected behavior. If you read the error, you’ll notice it’s the error you created with the message: “Something went wrong”.

Key points

  • The Model View Presenter pattern makes it possible to verify the behavior of the Presenter to ensure that it sticks to the contract expected between it and the View and Model
  • Use Mockito’s ArgumentCaptors to test Kotlin code, you must override the capture() method with your own custom version — one that can get around Kotlin’s null safety requirements.
  • Create TestRules to test code containing RxJava’s AndroidSchedulers. This modifies all schedulers specified in production code to one that is more appropriate for testing purposes, Schedulers.trampoline().
  • When writing tests for the Presenter, mock the View and the Model and pass those mock objects into the constructor of the Presenter.
  • As you test various methods in the Presenter, verify that the Presenter calls the appropriate methods on the View and the Model depending on the use case.
  • Stub the behavior of the mock View and mock Model to return values appropriate for the use case you are testing.

Where to go from here?

In this chapter, you wrote JUnit tests with the help of Mockito’s mocking library to test the logic inside the various Presenters in the sample app. Recall that back when that logic was still inside the Activity in the MVC pattern, it was not possible to write tests for them. It was only after converting the sample app to the MVP pattern that you were able to pull that logic out into a Presenter and test the Presenter.

Using the MVP pattern is only one way to achieve testability in your app. In the next chapter, you’ll learn other patterns that allows you to unit test your app. Choosing what pattern to use ultimately comes down to what pattern is the best fit for your particular app, though the end goals are still the same: separation of concerns and unit testability throughout the app.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.