Unit tests with mocks in 6 minutes
When conducting live coding interviews, I see many people who struggle with using Mocks in unit tests. Mocking is a process of simulating code behaviour without actually implementing it (under the hood it is done via Java Reflection). If your class depends on other classes, you don’t have to implement them for tests; instead, you can simulate their behavior. This way, you can test each code layer independently.
In this short article, I am going to cover 6 operators and keywords you need to write unit tests in modern Android projects. While I won’t be discussing stubs here, as they deserve a separate article, I will instead focus on the topic of mocking.
Quick setup
Simply add a dependency to Mockito-kotlin. You can find an instruction in their repo.
In your Android project, you usually have something like this: a Repository which handles data logic, a Retrofit service which loads values from the backend, and some Cache to call the backend less often. In our example Repository, we have a method to get a nickname, which returns either the cached value or, if it’s not present, the value from the server:
interface Service {
@GET("/user/current/nickname")
suspend fun getNicknameFromServer(): String
}
interface Storage {
fun putNicknameToCache(nickname: String)
fun getNicknameFromCache(): String?
}
class Repository(
private val service: Service,
private val storage: Storage,
) {
suspend fun getNickname(): String {
val nicknameFromCache = storage.getNicknameFromCache()
return if (nicknameFromCache == null) {
val nicknameFromBackend = service.getNicknameFromServer()
storage.putNicknameToCache(nicknameFromBackend)
nicknameFromBackend
} else {
nicknameFromCache
}
}
}
It is time-consuming to unit test this repository if you have to create special instances for all dependencies (imagine you have not only 2 dependencies but 10). Mocks can simplify it significantly. Let’s cover it with unit tests using mocks.
Whenever and Verify
These 2 operators are enough to write 99% of unit tests. For me, they are like Bonnie and Clyde — they always come together.
First, let’s define a test structure and initialise each dependency (you don’t have to create an instance of the class, mock() does it for you):
class RepositoryTest {
private val service: Service = mock()
private val storage: Storage = mock()
private val repository = Repository(
service = service,
storage = storage,
)
}
Let’s cover a scenario where the value is present in the cache. In this case, we need to return it and ensure we haven’t called the backend:
@Test
fun `GIVEN nickname present in storage WHEN getNickname THEN return value from storage AND don't call backend`() = runTest {
val expectedNickname = "JohnDoe"
whenever(storage.getNicknameFromCache()).thenReturn(expectedNickname)
val nickname = repository.getNickname()
Assert.assertEquals(expectedNickname, nickname)
verify(service, never()).getNicknameFromServer()
}
whenever(storage.getNicknameFromCache()).thenReturn(expectedNickname)
This is our first interaction with a mock. We define a rule, what class storage should do when getNicknameFromCache is called. thenReturn specifies the value that will be returned, in our case it is expectedNickname.
On every call of getNicknameFromCache you will receive expectedNickname — clear and simple.
After we mocked return value, now we should interact with the mocked object and validate that everything works correctly.
Assert.assertEquals(expectedNickname, nickname)
Common unit tests assertion, checks that 2 values are equal. In our case a value we mocked in Storage and a value we received from Repository.
verify(service, never()).getNicknameFromServer()
Mockito operator verify allows us to check how many times and with what arguments methods have been invoked. You have to pass your mocked class (service in our case), how many times it is supposed to be invoked (in our case never()) and which method was or wasn’t invoked — getNicknameFromServer.
Let me give you an example of the test when the value wasn’t present in the cache, so we had to load the nickname from the backend and put it into the cache:
@Test
fun `GIVEN nickname not present in storage WHEN getNickname THEN return value from backend AND put to storage`() = runTest {
val expectedNickname = "JohnDoe"
whenever(service.getNicknameFromServer()).thenReturn(expectedNickname)
whenever(storage.getNicknameFromCache()).thenReturn(null)
val nickname = repository.getNickname()
Assert.assertEquals(expectedNickname, nickname)
verify(service, times(1)).getNicknameFromServer()
verify(storage, times(1)).putNicknameToCache(expectedNickname)
}
Try to understand for yourself what each line does in this test.
From my experience, these 2 operators are enough to write 99% of unit tests. There are very rare scenarios in which you need something additional, such as:
ArgumentCaptor, Any and Eq
ArgumentCaptor gives you an exact argument that was used during interaction with the mocked class. You can use Any (or its friend AnyOrNull) as a placeholder for an argument when you don’t care about its specific value. Within a single interaction, if you used ArgumentCaptor or Any, exact arguments must be wrapped into eq(value), like eq(“JohnDoe”).
For example, we are uploading a new avatar, username, and the current timestamp to the backend:
interface Service {
@POST("/user/current/avatar")
suspend fun uploadAvatarToServer(
@Part file: MultipartBody.Part,
@Part username: String,
@Part timestamp: Long,
)
}
class Repository(
private val service: Service,
) {
suspend fun uploadAvatar(file: File) {
service.uploadAvatarToServer(
file = MultipartBody.Part.createFormData(
name = "avatar",
filename = file.name,
body = file.asRequestBody("image/jpeg".toMediaType()),
),
username = "JohnDoe",
timestamp = System.currentTimeMillis(),
)
}
}
MultipartBody.Part is a Java class which doesn’t implement equals and hashcode. If you try to manually compare them, you will always get false. In this case we have to use ArgumentCaptor to assert values inside MultipartBody.Part:
@Test
fun `WHEN uploadAvatar THEN request body with file is configured correctly`() = runTest {
val fileName = "myselfie.jpeg"
val expectedHeader = "form-data; name=\"avatar\"; filename=\"$fileName\""
val expectedMediaType = "image/jpeg".toMediaType()
repository.uploadAvatar(File(fileName))
val argumentCaptor = argumentCaptor<MultipartBody.Part>()
verify(service).uploadAvatarToServer(
file = argumentCaptor.capture(),
user = eq("JohnDoe"), // Validate correct argument using eq
timestamp = any(), // In this test we don't care about the timestamp
)
val partFromRequest: MultipartBody.Part = argumentCaptor.firstValue
val headerFromRequest = partFromRequest.headers!!["Content-Disposition"]
Assert.assertEquals(expectedHeader, headerFromRequest)
Assert.assertEquals(expectedMediaType, partFromRequest.body.contentType())
}
val argumentCaptor = argumentCaptor<MultipartBody.Part>()
You have to specify the type for the argumentCaptor.
file = argumentCaptor.capture(),
Call capture() within a mock where you need to assert a value.
val partFromRequest: MultipartBody.Part = argumentCaptor.firstValue Assert.assertEquals(expectedMediaType, partFromRequest.body.contentType())
Write common assertions to make sure your code is correct.
Spy
Spy creates a copy of your instance and adds the possibility to use whenever and verify. You can use Spy on any dependency you’re going to provide as a real implementation (although I don’t recommend it) or on your testable object itself:
internal class RepositoryTest {
private val service: Service = mock()
private val storage: Storage = spy(StorageImpl())
private val repository = spy(
Repository(
service = service,
storage = storage,
)
)
}
I personally use spy only for testing high-level logic. Imagine you have a method which calls other methods at the same class:
fun changeNickname(nickname: String) {
changeNicknameOnBackend(nickname)
updateNicknameInCache(nickname)
trackNicknameChanged()
}
fun changeNicknameOnBackend(nickname: String) { ... }
fun updateNicknameInCache(nickname: String) { ... }
fun trackNicknameChanged() { ... }
You can test every method independently and for changeNickname use spy:
private val repository = spy(Repository())
@Test
fun `WHEN changeNickname THEN call correct methods`() {
val expectedNickname = "JohnDoe"
repository.changeNickname(expectedNickname)
verify(repository).changeNicknameOnBackend(expectedNickname)
verify(repository).updateNicknameInCache(expectedNickname)
verify(repository).trackNicknameChanged()
}
Mocking of Static code
If you use static invocations (from my experience most common are UUID and Intent.fromChooser), you can test it using Mockito.mockStatic(). Let’s take a look at this code:
suspend fun changeNickname(newNickname: String) {
service.changeNickname(
nickname = newNickname,
uuid = UUID.randomUUID().toString(),
)
}
Test for this method looks like this:
@Test
fun `WHEN changeNickname THEN send correct params to backend`() = runTest {
Mockito.mockStatic(UUID::class.java).use { mockedUUID ->
val expectedNickname = "JohnDoe"
val uuid = mock<UUID>()
whenever(uuid.toString()).thenReturn("uuid")
mockedUUID.`when`<Any> { UUID.randomUUID() }.thenReturn(uuid)
repository.changeNickname(expectedNickname)
verify(service).changeNickname(
nickname = expectedNickname,
uuid = "uuid",
)
}
}
mockedUUID.`when`<Any> { UUID.randomUUID() }.thenReturn(uuid)
You mock static methods of class UUID::class.java and specify what to return on the invocation of randomUUID
Conclusion
If you don’t write unit tests, develop a good habit of spending 30 minutes after implementation to cover code with tests. No matter which framework you use (Mockito, MockK, or PowerMock) — it is an extra layer of security that ensures you haven’t missed anything or made a mistake. Tests also help you with refactoring the code (tests should remain green after refactoring), and specifically, mocks help us test every layer independently.