Employing Mocks
In unit tests, we often run into problems where we want to exercise one thing, but that thing depends upon other things that we lack:
- It depends upon the Android SDK
- It depends upon a server, and that server might not always be running at the time we would want to run the tests
- It depends upon content that does not exist in an isolated test, such as some sort of authentication token representing a logged-in user
For those things, we can use mocks.
Why Are We Being Mean?
In this case, “mock” does not mean to ridicule something.
Instead, here, “mock” is used to indicate an object that pretends to be something that it is not:
- It pretends to be an Android
Context
- It pretends to be your repository that ordinarily would talk to some server
- It pretends to be an authenticated user
If we set things up properly, our tests can exercise some specific functionality while using mock objects for anything that we cannot readily use in our tests. We can set up the mock objects to respond as we need them to for a given test (“stubs”). We can even verify that those mock objects were used when we expected them to be.
The Importance of Dependency Inversion
Part of the “set things up properly” is needing to ensure we have a way of getting the mocks where we need them. That is where dependency inversion comes into play.
In ToDoTests
, we have a ViewModel
class called SingleModelMotor
. It works with a ToDoRepository
to supply the UI with to-do items, plus support actions whereby users can add, edit, or delete to-do items. In the real app, ToDoRepository
is backed by a Room-powered database. However, that does not work well in a unit test: Room expects to work with an Android Context
object, and Room expects a SQLite implementation that exists on Android but not necessarily on your development machine.
We could use a mock ToDoRepository
… but that implies that we have a way to get the mock repository over to our SingleModelMotor
that we are testing. If SingleModelMotor
was referring to some singleton instance of ToDoRepository
— such as having ToDoRepository
be a Kotlin object
— then we would be out of luck.
Instead, ToDoTests
uses Koin for dependency inversion, as we used in a previous chapter. As such, SingleModelMotor
gets a ToDoRepository
in its constructor, so in testing, we can just supply our mock ToDoRepository
when we create a SingleModelMotor
instance.
Add Mockito
The leading mock implementation in use for Android is Mockito. A Java project can use Mockito directly, but a Kotlin project usually is better served by using Mockito by means of the Mockito-Kotlin wrapper library.
ToDoTests
has testImplementation
lines in its dependencies
to pull in Mockito and the Mockito-Kotlin wrapper:
testImplementation "org.mockito:mockito-inline:3.12.1"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
Define and Supply a Mock
Declaring a mock in Kotlin is very easy: call the mock()
global function supplied by Mockito-Kotlin:
private val repo: ToDoRepository = mock {
on { find(testModel.id) } doReturn flowOf(testModel)
}
The resulting object is not a “real” instance of ToDoRepository
, but rather an generated object that offers the same public API.
We can then pass this mock to objects that need it, such as an instance of SingleModelMotor
:
@Before
fun setUp() {
underTest = SingleModelMotor(repo, testModel.id)
}
Here, setUp()
will be called before each of our test functions, since it has the @Before
annotation.
Define Stub Responses
The lambda expression in our mock()
call defines a stub response. Our SingleModelMotor
is going to call a find()
function on ToDoRepository
, and we need to indicate what our mock should return when that call is made. By default, the mock will return null
, which is not what we want.
One Mockito-Kotlin recipe for setting up a stub uses on()
and doReturn()
inside of that mock()
lambda expression. on()
wraps the API call that we are expecting (repo.find(testModel.id)
), where testModel
is a test ToDoModel
instance:
private val testModel = ToDoModel("this is a test")
The doReturn()
function indicates the value that we want to return when that API call is made. In this case, find()
returns a Flow
from Kotlin coroutines, and we use the flowOf()
top-level function to create a Flow
wrapped around our single test model object.
Flow
in the "Introducing Flows and Channels" chapter of Elements of Kotlin Coroutines!
The net result of this line is that when SingleModelMotor
tries calling find()
on our ToDoRepository
, and it passes in the stated id
value, our mock will return a Flow
of our test model object.
Verify Calls
We can even confirm that SingleModelMotor
actually calls find()
as we are expecting. We do that in the initial state()
test function:
@Test
fun `actions pass through to repo`() {
val replacement = testModel.copy("whatevs")
underTest.save(replacement)
mainDispatcherRule.dispatcher.runCurrent()
mainDispatcherRule.dispatcher.runBlockingTest { verify(repo).save(replacement) }
underTest.delete(replacement)
mainDispatcherRule.dispatcher.runCurrent()
mainDispatcherRule.dispatcher.runBlockingTest { verify(repo).delete(replacement) }
}
The final line of that function starts with verify()
. This wraps a mock object with what amounts to another mock, one that is used for verification. When we call functions on the verify()
mock, that mock confirm that the “real” mock object received a similar call during our test. If it has not, the test fails.
Using Rules
If we look at the entirety of SimpleModelMotorTest
, we see that along with our setUp()
function and the test function we have been examining, we have another test function… and a strange @Rule
thing:
package com.commonsware.todo.ui
import com.commonsware.todo.MainDispatcherRule
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class SingleModelMotorTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule(paused = true)
private val testModel = ToDoModel("this is a test")
private val repo: ToDoRepository = mock {
on { find(testModel.id) } doReturn flowOf(testModel)
}
private lateinit var underTest: SingleModelMotor
@Before
fun setUp() {
underTest = SingleModelMotor(repo, testModel.id)
}
@Test
fun `initial state`() {
mainDispatcherRule.dispatcher.runCurrent()
mainDispatcherRule.dispatcher.runBlockingTest {
val item = underTest.states.first().item
assertEquals(testModel, item)
}
}
@Test
fun `actions pass through to repo`() {
val replacement = testModel.copy("whatevs")
underTest.save(replacement)
mainDispatcherRule.dispatcher.runCurrent()
mainDispatcherRule.dispatcher.runBlockingTest { verify(repo).save(replacement) }
underTest.delete(replacement)
mainDispatcherRule.dispatcher.runCurrent()
mainDispatcherRule.dispatcher.runBlockingTest { verify(repo).delete(replacement) }
}
}
As mentioned earlier, JUnit rules represent encapsulated code that runs before and after each of our tests. In Kotlin, due to some JUnit4 quirks, we need to use @get:Rule
syntax, saying that the @Rule
annotation is being applied to the getter function for this property.
Sometimes, libraries provide us with rules. For example, InstantTaskExecutorRule
is from the Jetpack. It changes the behavior of LiveData
such that it always uses the current thread for its work, rather than the Android main application thread. All we would need to do is instantiate and annotate the InstantTaskExecutorRule
and we get this behavior. But, this project does not use LiveData
, so we do not need that particular rule.
Writing Rules
However, we could use a similar rule for coroutines. SingleModelMotor
references Dispatchers.Main
, which in Android will map to the main application thread. However, Dispatchers.Main
has no meaning in a unit test, as we are not running on Android. We need to define what to use for Dispatchers.Main
when running our tests, and ideally we would use something that makes the tests easier to run.
To that end, the ToDoTests
project has a MainDispatcherRule
, which we use in SingleModelMotorTest
:
package com.commonsware.todo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
// inspired by https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471
class MainDispatcherRule(paused: Boolean) : TestWatcher() {
val dispatcher =
TestCoroutineDispatcher().apply { if (paused) pauseDispatcher() }
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
dispatcher.cleanupTestCoroutines()
}
}
The simplest way to write a JUnit rule is to extend TestWatcher
, then override the starting()
and finishing()
functions. Those will be called before and after each test, respectively.
In the case of MainDispatcherRule
, we:
- Create a
TestCoroutineDispatcher
- Call
pauseDispatcher()
on theTestCoroutineDispatcher
- Call
Dispatchers.setMain()
before the test to haveDispatchers.Main
point to thisTestCoroutineDispatcher
- Call
Dispatchers.resetMain()
after the test to remove the reference to theTestCoroutineDispatcher
- Clean up the
TestCoroutineDispatcher
after the test
A TestCoroutineDispatcher
is a coroutine dispatcher that operates under manual control, so our test code can indicate when it should process any queued coroutines. And, those coroutines run synchronously, on whatever thread we happen to be on. That is the behavior of TestCoroutineDispatcher
when it is in a “paused” state — we put it in that state at the outset via the pauseDispatcher()
call.
Then, in our SingleModelMotorTest
, we can apply this rule using @get:Rule
syntax. Also, we can call runCurrent()
on the TestCoroutineDispatcher
to indicate that it is time to run any queued coroutines.
Testing StateFlow
The ToDoTests
project uses a mix of coroutines and StateFlow
for its threading and reactive results. In particular, SingleModelMotor
has a states
property that is a StateFlow
, emitting some viewstates that are created by means of that find()
call on our repository.
So the overall flow of the initial state()
test function is:
- Use
runCurrent()
to execute any coroutines that might have been invoked by creating ourSingleModelMotor
in initializers or its constructor - Use
runBlockingTest()
to execute a coroutine that usesfirst()
to get the first viewstate and confirm that it equals an expected value
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.