9
Testing MVP
Written by Yun Cheng
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
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:
- Create a base test class to wrap the
capture()
functionality for MockitoArgumentCaptors
. - 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 thecapture()
method with your own custom version — one that can get around Kotlin’snull
safety requirements. - Create
TestRule
s to test code containing RxJava’sAndroidSchedulers
. 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.