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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

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'
})

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
open class BaseTest {  
  open fun <T> captureArg(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
}

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().

Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
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()  
        }  
      }  
    }  
  }  
}

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.

@RunWith(MockitoJUnitRunner::class)  
class MainPresenterTests : BaseTest() {  
  @Rule @JvmField var testSchedulerRule = RxImmediateSchedulerRule()  
}
@Mock  
private lateinit var mockActivity : MainContract.ViewInterface  

@Mock  
private lateinit var mockDataSource : LocalDataSource  

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

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  
}
@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)
}
@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()  
}

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.

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  
  }
@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")  
}
@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")  
}

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.

//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)  
  }
}

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.

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

  //2
  Mockito.verify(mockActivity).displayError("Movie title cannot be empty")  
}
//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()  
}

Testing the SearchPresenter

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

@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)  
  }
}
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)  
  }
@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)
}
@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")  
}

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.

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now